@happyvertical/smrt-personas 0.38.18

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/AGENTS.md ADDED
@@ -0,0 +1,85 @@
1
+ # @happyvertical/smrt-personas
2
+
3
+ Tenant-owned, context-scoped agent personas and their resolution. Layers over
4
+ `@happyvertical/smrt-agents`' `TenantAgent` availability/ceiling gate to decide
5
+ *how* an agent behaves for a given tenant and context (its `TenantAgent` binding
6
+ decides *whether* it runs and caps its capabilities).
7
+
8
+ This is the L2 foundation of the "learning agents" epic (#1885). Foundation
9
+ only: `AgentPersona` + `PersonaResolver`. `ExecuteAsPrincipal`, feedback, the
10
+ reflection runner, and multi-instance routing are separate issues
11
+ (#1888/#1889/#1890).
12
+
13
+ ## Models
14
+
15
+ - **AgentPersona** (`@TenantScoped({ mode: 'required' })`, table `agent_personas`):
16
+ a behavioral profile for one agent class within one tenant. Many per
17
+ `(tenant, agentClass)`; unique on `(tenant_id, agent_class, name)` via
18
+ `conflictColumns`. Fields:
19
+ - `agentClass` — canonical qualified agent type (e.g.
20
+ `@happyvertical/smrt-agents:Praeco`); stored canonical so it lines up with
21
+ `TenantAgent.agentClass`.
22
+ - `name` — persona name, unique per `(tenant, agentClass)`.
23
+ - `contextType` / `contextId` — optional scoping. No `contextType` = a
24
+ tenant-wide default; `contextType` only = type-scoped; both = exact context.
25
+ - `instructions` — system prompt layered onto the agent.
26
+ - `allowedTools` — JSON array of tool ids stored as a string; use
27
+ `getAllowedTools()` / `setAllowedTools()` (tolerant `try/catch` parse).
28
+ - `runAsUserId` — `@crossPackageRef('@happyvertical/smrt-users:User')`, the
29
+ principal the persona runs as.
30
+ - `actsAsProfileId` — optional `@crossPackageRef('@happyvertical/smrt-profiles:Profile')`,
31
+ a `Bot` identity for audit/attribution.
32
+ - `memoryScope` — memory/reflection partition key (empty → resolver-derived).
33
+ - `priority` — integer; higher wins among personas that apply at the same
34
+ tenant level.
35
+ - `enabled` — boolean; only enabled personas are resolved.
36
+
37
+ ## Collection
38
+
39
+ `AgentPersonaCollection`:
40
+
41
+ - `byTenantAndClass(tenantId, agentClass)` — all personas for the pair
42
+ (canonicalized), ordered by descending priority then name.
43
+ - `findActive(tenantId, agentClass, context?)` — enabled personas applicable to
44
+ a context, same ordering.
45
+
46
+ ## PersonaResolver
47
+
48
+ `resolve(tenantId, agentClass, context, options) → ResolvedPersona` layers,
49
+ bottom to top: **manifest defaults → `TenantAgent` gate/ceiling → AgentPersona**.
50
+
51
+ - **Hierarchy inheritance** mirrors `TenantAgentCollection.resolveForTenant`:
52
+ walks the requesting tenant then its ancestors (`options.getAncestorIds`,
53
+ nearest first). The nearest level with any applicable persona wins (a closer
54
+ level shadows a farther one).
55
+ - **Selection within a level**: most context-specific first
56
+ (exact > type-scoped > tenant-wide), then highest `priority`, then name.
57
+ - **Ceiling**: `options.availability` (a `PersonaAvailabilityGate`) gates
58
+ availability and intersects the resolved `allowedTools`. Build one from a
59
+ `ResolvedAgentAvailability` with `availabilityFromResolvedAgent()`.
60
+ - **Default fallback**: when no persona applies, returns a defined default
61
+ (`source: 'default'`, `name: 'default'`) from manifest defaults + the gate.
62
+
63
+ Because the walk reads personas across tenants, `resolve()` is intended for a
64
+ system/admin path where cross-tenant reads are allowed — not inside a strict
65
+ per-tenant interceptor context (same expectation as `resolveForTenant`).
66
+
67
+ ## Dependencies
68
+
69
+ Leaf package (acyclic by construction — nothing depends back on it):
70
+
71
+ - Runtime: `@happyvertical/smrt-core`, `@happyvertical/smrt-tenancy`,
72
+ `@happyvertical/smrt-agents` (the `TenantAgent` type it bridges).
73
+ - `runAsUserId` / `actsAsProfileId` reference `@happyvertical/smrt-users` and
74
+ `@happyvertical/smrt-profiles` via `@crossPackageRef` string ids only — no
75
+ package edge (keeps the DAG minimal). No inter-`smrt-*` `peerDependencies`.
76
+
77
+ ## Gotchas
78
+
79
+ - **`conflictColumns` replaces the default `(slug, context)` unique index** — the
80
+ unique key is `(tenant_id, agent_class, name)`, so same-named personas across
81
+ tenants are allowed and tenant isolation is preserved.
82
+ - **`allowedTools` is text, not `type: 'json'`** — stored as a JSON string and
83
+ read through the helpers, so it never auto-hydrates to an object.
84
+ - **Resolver runs outside strict tenant context** — it deliberately crosses
85
+ tenants to walk the hierarchy.
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,294 @@
1
+ import { ResolvedAgentAvailability } from '@happyvertical/smrt-agents';
2
+ import { SmrtCollection } from '@happyvertical/smrt-core';
3
+ import { SmrtObject } from '@happyvertical/smrt-core';
4
+
5
+ /**
6
+ * AgentPersona SmrtObject — a tenant/context-scoped behavioral profile for an
7
+ * agent class.
8
+ */
9
+ export declare class AgentPersona extends SmrtObject {
10
+ /** Owning tenant (required — personas are never global). */
11
+ tenantId: string;
12
+ /**
13
+ * Canonical qualified agent type this persona configures
14
+ * (e.g. `@happyvertical/smrt-agents:Praeco`). Stored in canonical form so it
15
+ * lines up with `TenantAgent.agentClass`.
16
+ */
17
+ agentClass: string;
18
+ /**
19
+ * Human-readable persona name, unique per `(tenant, agentClass)`
20
+ * (see `conflictColumns`). Inherited `name` from {@link SmrtObject} is the
21
+ * conflict column; declared here for documentation and typing clarity.
22
+ */
23
+ name: string;
24
+ /**
25
+ * Optional context dimension type this persona is scoped to
26
+ * (e.g. `'site'`, `'property'`, `'chatRoom'`). `null` means the persona is a
27
+ * tenant-wide default that applies to every context for its agent class.
28
+ */
29
+ contextType: string | null;
30
+ /**
31
+ * Optional context dimension id. Only meaningful alongside `contextType`:
32
+ * when set, the persona targets one specific context row; when `null` (with
33
+ * a `contextType` set) it applies to every context of that type.
34
+ */
35
+ contextId: string | null;
36
+ /** System prompt / behavioral instructions layered onto the agent. */
37
+ instructions: string;
38
+ /**
39
+ * Allowed tool identifiers, stored as a JSON array string. Use
40
+ * {@link getAllowedTools} / {@link setAllowedTools} rather than reading this
41
+ * field directly. Empty array (`'[]'`) means "no persona-specific tools"
42
+ * (the resolver then falls back to manifest defaults, capped by the
43
+ * `TenantAgent` ceiling).
44
+ */
45
+ allowedTools: string;
46
+ /**
47
+ * The user whose principal this persona runs as (cross-package reference to
48
+ * `@happyvertical/smrt-users:User`). Required — a persona always runs as a
49
+ * concrete principal. Stored as a plain id column; no DDL FK constraint is
50
+ * emitted (cross-package).
51
+ */
52
+ runAsUserId: string;
53
+ /**
54
+ * Optional `Bot` profile the persona acts as for identity/audit
55
+ * (cross-package reference to `@happyvertical/smrt-profiles:Profile`).
56
+ * `null` when the persona has no distinct acting identity.
57
+ */
58
+ actsAsProfileId: string | null;
59
+ /**
60
+ * Memory scope key controlling how the agent's memory/reflection is
61
+ * partitioned for this persona. Empty string means "use a resolver-derived
62
+ * default".
63
+ */
64
+ memoryScope: string;
65
+ /**
66
+ * Resolution priority — higher wins when several personas apply to the same
67
+ * context at the same tenant level. Integer.
68
+ */
69
+ priority: number;
70
+ /** Whether this persona is active and eligible for resolution. */
71
+ enabled: boolean;
72
+ /**
73
+ * Allowed tool identifiers as an array.
74
+ *
75
+ * Tolerates malformed stored JSON by returning an empty array.
76
+ */
77
+ getAllowedTools(): string[];
78
+ /**
79
+ * Replace the allowed tool identifiers.
80
+ */
81
+ setAllowedTools(tools: string[]): void;
82
+ }
83
+
84
+ /**
85
+ * Collection for managing {@link AgentPersona} records.
86
+ */
87
+ export declare class AgentPersonaCollection extends SmrtCollection<AgentPersona> {
88
+ static readonly _itemClass: typeof AgentPersona;
89
+ /**
90
+ * List personas for a tenant + agent class, matching either the canonical
91
+ * qualified name or the simple class name (see {@link agentClassAliases}).
92
+ *
93
+ * Ordered by descending priority then name.
94
+ *
95
+ * @param extraWhere - Additional equality filters merged into the query
96
+ * (e.g. `{ enabled: true }`).
97
+ */
98
+ private listForClass;
99
+ /**
100
+ * List every persona bound to a tenant + agent class.
101
+ *
102
+ * Ordered by descending priority then name so callers that don't need the
103
+ * full resolver still get a deterministic, priority-first ordering.
104
+ */
105
+ byTenantAndClass(tenantId: string, agentClass: string): Promise<AgentPersona[]>;
106
+ /**
107
+ * List the enabled personas for a tenant + agent class, filtered to those
108
+ * applicable to a context.
109
+ *
110
+ * `enabled` is filtered in the query; the context predicate (tenant-wide vs
111
+ * type-scoped vs exact) is applied in memory since it is an OR over several
112
+ * columns. A persona is applicable when it is a tenant-wide default (no
113
+ * `contextType`) or its context matches the requested one. Ordered by
114
+ * descending priority then name.
115
+ *
116
+ * @param context - Optional `{ contextType, contextId }` to filter by.
117
+ */
118
+ findActive(tenantId: string, agentClass: string, context?: {
119
+ contextType?: string | null;
120
+ contextId?: string | null;
121
+ }): Promise<AgentPersona[]>;
122
+ }
123
+
124
+ /**
125
+ * Project a `TenantAgent` resolution into a {@link PersonaAvailabilityGate}.
126
+ *
127
+ * This is the concrete bridge between the `@happyvertical/smrt-agents`
128
+ * availability layer and the persona layer. Pass `toolPermissionPrefix` to
129
+ * derive a tool ceiling from granted permission ids (permissions whose id
130
+ * starts with the prefix become the ceiling, with the prefix stripped); omit it
131
+ * to leave the ceiling open.
132
+ *
133
+ * @param resolved - A `ResolvedAgentAvailability` (or `null`/`undefined`).
134
+ * @param options.toolPermissionPrefix - Permission-id prefix that marks
135
+ * tool grants (e.g. `'tool:'`).
136
+ * @returns A gate, or `null` when `resolved` is nullish.
137
+ */
138
+ export declare function availabilityFromResolvedAgent(resolved: ResolvedAgentAvailability | null | undefined, options?: {
139
+ toolPermissionPrefix?: string;
140
+ }): PersonaAvailabilityGate | null;
141
+
142
+ /**
143
+ * Return the canonical agent type identifier for storage and lookup.
144
+ *
145
+ * Uses the registry's qualified name when the class is registered and falls
146
+ * back to the raw input for dynamically defined or unregistered classes. This
147
+ * mirrors `getAgentTypeName()` in `@happyvertical/smrt-agents` without reaching
148
+ * into that package's internal module — personas store the same canonical form
149
+ * that `TenantAgent` does so the two layers line up.
150
+ */
151
+ export declare function canonicalAgentClass(name: string): string;
152
+
153
+ /**
154
+ * Bottom-layer defaults for an agent class, typically derived from its build
155
+ * manifest.
156
+ */
157
+ export declare interface ManifestPersonaDefaults {
158
+ /** Default system instructions when no persona overrides them. */
159
+ instructions?: string;
160
+ /** Default tool identifiers, capped by the availability ceiling. */
161
+ allowedTools?: string[];
162
+ /** Default memory scope. */
163
+ memoryScope?: string;
164
+ }
165
+
166
+ /**
167
+ * Whether a persona applies to a requested context.
168
+ *
169
+ * - No `contextType` on the persona → tenant-wide default, applies to all.
170
+ * - `contextType` set, `contextId` null → type-scoped, applies when the
171
+ * requested `contextType` matches.
172
+ * - Both set → exact, applies when both match.
173
+ */
174
+ export declare function personaAppliesToContext(persona: Pick<AgentPersona, 'contextType' | 'contextId'>, context: {
175
+ contextType?: string | null;
176
+ contextId?: string | null;
177
+ }): boolean;
178
+
179
+ /**
180
+ * Middle-layer availability + capability ceiling, projected from a
181
+ * `TenantAgent` resolution.
182
+ */
183
+ export declare interface PersonaAvailabilityGate {
184
+ /** Resolved `TenantAgent` status. `'disabled'` marks the persona unavailable. */
185
+ status: 'active' | 'disabled';
186
+ /**
187
+ * Tool identifiers the tenant is permitted to grant. When set, resolved
188
+ * `allowedTools` are intersected with this ceiling. `undefined` means no
189
+ * ceiling (tools pass through unchanged).
190
+ */
191
+ toolCeiling?: string[];
192
+ /** Tenant the binding was resolved from (provenance only). */
193
+ sourceTenantId?: string;
194
+ }
195
+
196
+ /**
197
+ * The context a persona is being resolved for.
198
+ */
199
+ export declare interface PersonaContext {
200
+ /** Context dimension type (e.g. `'site'`). */
201
+ contextType?: string | null;
202
+ /** Context dimension id within `contextType`. */
203
+ contextId?: string | null;
204
+ }
205
+
206
+ /**
207
+ * Context-specificity rank used for resolution ordering.
208
+ *
209
+ * - `2` exact `(contextType, contextId)` match
210
+ * - `1` type-scoped match (`contextType` matches, persona `contextId` null)
211
+ * - `0` tenant-wide default (persona has no `contextType`)
212
+ */
213
+ export declare function personaContextRank(persona: Pick<AgentPersona, 'contextType' | 'contextId'>): number;
214
+
215
+ /**
216
+ * Resolves the active persona for a `(tenant, agentClass, context)` request.
217
+ */
218
+ export declare class PersonaResolver {
219
+ private readonly personas;
220
+ constructor(personas: AgentPersonaCollection);
221
+ /**
222
+ * Resolve the active persona.
223
+ *
224
+ * @param tenantId - Requesting tenant.
225
+ * @param agentClass - Agent class (canonicalized internally).
226
+ * @param context - Context dimension being resolved for.
227
+ * @param options - Hierarchy walk, manifest defaults, and availability gate.
228
+ * @returns The layered {@link ResolvedPersona} — either a matched persona or
229
+ * the defined default fallback.
230
+ */
231
+ resolve(tenantId: string, agentClass: string, context?: PersonaContext, options?: ResolvePersonaOptions): Promise<ResolvedPersona>;
232
+ private layerPersona;
233
+ private defaultResolution;
234
+ }
235
+
236
+ /**
237
+ * A fully resolved persona: the layered, ready-to-use configuration plus
238
+ * provenance describing how it was resolved.
239
+ */
240
+ export declare interface ResolvedPersona {
241
+ /** Canonical qualified agent type the resolution was for. */
242
+ agentClass: string;
243
+ /** Tenant the resolution was requested for. */
244
+ tenantId: string;
245
+ /** Id of the selected persona, or `undefined` for the default fallback. */
246
+ personaId?: string;
247
+ /** Selected persona name, or `'default'` for the fallback. */
248
+ name: string;
249
+ /** Layered instructions (persona → manifest default). */
250
+ instructions: string;
251
+ /** Layered tool ids (persona/manifest tools, capped by the ceiling). */
252
+ allowedTools: string[];
253
+ /** Run-as user principal, if the selected persona sets one. */
254
+ runAsUserId?: string;
255
+ /** Acting `Bot` profile id, if the selected persona sets one. */
256
+ actsAsProfileId?: string | null;
257
+ /** Resolved memory scope (persona → manifest default → derived default). */
258
+ memoryScope: string;
259
+ /** Selected persona priority (`0` for the default fallback). */
260
+ priority: number;
261
+ /** Context type the resolution was requested for. */
262
+ contextType?: string | null;
263
+ /** Context id the resolution was requested for. */
264
+ contextId?: string | null;
265
+ /** Whether a persona matched (`'persona'`) or the default was used (`'default'`). */
266
+ source: 'persona' | 'default';
267
+ /** For a matched persona, whether it was the tenant's own or inherited. */
268
+ personaSource?: 'explicit' | 'inherited';
269
+ /** Tenant the matched persona came from (own tenant or an ancestor). */
270
+ sourceTenantId?: string;
271
+ /** Whether the `TenantAgent` gate reports the agent as available. */
272
+ available: boolean;
273
+ }
274
+
275
+ /**
276
+ * Options for {@link PersonaResolver.resolve}.
277
+ */
278
+ export declare interface ResolvePersonaOptions {
279
+ /**
280
+ * Return ancestor tenant ids for a tenant, nearest parent first through to
281
+ * the root — mirrors `TenantAgentCollection.resolveForTenant`. Omit for a
282
+ * single-tenant resolution with no inheritance.
283
+ */
284
+ getAncestorIds?: (tenantId: string) => Promise<string[]>;
285
+ /** Bottom-layer manifest defaults for the agent class. */
286
+ manifestDefaults?: ManifestPersonaDefaults;
287
+ /**
288
+ * Middle-layer `TenantAgent` availability + ceiling. Omit to skip gating
289
+ * (the result is always `available: true` with no tool ceiling).
290
+ */
291
+ availability?: PersonaAvailabilityGate | null;
292
+ }
293
+
294
+ export { }