@happyvertical/smrt-personas 0.38.19 → 0.38.21

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.
Files changed (39) hide show
  1. package/AGENTS.md +119 -7
  2. package/dist/__smrt-register__.d.ts +2 -0
  3. package/dist/__smrt-register__.d.ts.map +1 -0
  4. package/dist/agent-persona.d.ts +161 -0
  5. package/dist/agent-persona.d.ts.map +1 -0
  6. package/dist/directive-approval.d.ts +94 -0
  7. package/dist/directive-approval.d.ts.map +1 -0
  8. package/dist/directive-principal.d.ts +56 -0
  9. package/dist/directive-principal.d.ts.map +1 -0
  10. package/dist/directive-proposal.d.ts +103 -0
  11. package/dist/directive-proposal.d.ts.map +1 -0
  12. package/dist/feedback.d.ts +127 -0
  13. package/dist/feedback.d.ts.map +1 -0
  14. package/dist/index.d.ts +11 -294
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +829 -19
  17. package/dist/index.js.map +1 -1
  18. package/dist/manifest.json +919 -3
  19. package/dist/persona-instance.d.ts +249 -0
  20. package/dist/persona-instance.d.ts.map +1 -0
  21. package/dist/persona-memory.d.ts +45 -0
  22. package/dist/persona-memory.d.ts.map +1 -0
  23. package/dist/persona-prompt.d.ts +77 -0
  24. package/dist/persona-prompt.d.ts.map +1 -0
  25. package/dist/persona-resolver.d.ts +134 -0
  26. package/dist/persona-resolver.d.ts.map +1 -0
  27. package/dist/reflection-runner.d.ts +113 -0
  28. package/dist/reflection-runner.d.ts.map +1 -0
  29. package/dist/smrt-knowledge.json +519 -11
  30. package/dist/svelte/components/DirectiveReviewQueue.svelte +185 -0
  31. package/dist/svelte/components/DirectiveReviewQueue.svelte.d.ts +15 -0
  32. package/dist/svelte/components/DirectiveReviewQueue.svelte.d.ts.map +1 -0
  33. package/dist/svelte/index.d.ts +16 -0
  34. package/dist/svelte/index.d.ts.map +1 -0
  35. package/dist/svelte/index.js +13 -0
  36. package/dist/svelte/types.d.ts +46 -0
  37. package/dist/svelte/types.d.ts.map +1 -0
  38. package/dist/svelte/types.js +39 -0
  39. package/package.json +23 -7
package/AGENTS.md CHANGED
@@ -5,10 +5,13 @@ Tenant-owned, context-scoped agent personas and their resolution. Layers over
5
5
  *how* an agent behaves for a given tenant and context (its `TenantAgent` binding
6
6
  decides *whether* it runs and caps its capabilities).
7
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).
8
+ This is the L2 layer of the "learning agents" epic (#1885): the persona foundation
9
+ (`AgentPersona` + `PersonaResolver`), the **persona learning & adaptation loop**
10
+ (#1889) feedback → confidence-scored reinforcement a scheduled reflection
11
+ runner that emits pending directive proposals → a permission-gated approval
12
+ surface that activates an approved rewrite as a tenant/persona-scoped prompt
13
+ override — and the persona-as-durable-instance / multi-instance helpers (#1890,
14
+ below). `ExecuteAsPrincipal` remains a separate issue (#1888).
12
15
 
13
16
  ## Models
14
17
 
@@ -34,6 +37,67 @@ reflection runner, and multi-instance routing are separate issues
34
37
  tenant level.
35
38
  - `enabled` — boolean; only enabled personas are resolved.
36
39
 
40
+ - **Feedback** (`@TenantScoped({ mode: 'optional' })`, table `agent_feedback`):
41
+ one reinforcement signal against a persona and the learning episode it judges.
42
+ Human signals (`accept` / `reject` / `correction` / `rating`) and autonomous
43
+ signals (`outcome` / `metric`) each carry a required **`correlationId`** (+
44
+ optional `correlationType`) back to the AI call / dispatch / job. `scope` /
45
+ `key` name the `LearningMemory` episode; `toLearningOutcome()` maps the signal
46
+ onto a `LearningOutcome` (feeds reinforcement, #1886). `FeedbackCollection`:
47
+ `forPersona`, `forScope`, `byCorrelation`.
48
+
49
+ - **DirectiveProposal** (`@TenantScoped({ mode: 'required' })`, table
50
+ `directive_proposals`): a pending, human-reviewable proposed edit to a
51
+ persona's `instructions`. Read-only generated surface (`api`/`cli`/`mcp` =
52
+ `list`/`get`) — writes go through the gated approval service, not CRUD. Key
53
+ fields: `promptKey`, `proposedInstructions`, `currentInstructions` (diff),
54
+ `rationale`, `evidence` (JSON: episode + feedback ids), `status`
55
+ (`pending`/`approved`/`rejected`/`superseded`), `fingerprint` (dedup so a
56
+ rejected rewrite is never re-surfaced), `proposedBy`, `reviewedBy` /
57
+ `reviewedAt` / `activatedOverrideId`. `DirectiveProposalCollection`:
58
+ `pending(...)` (the review queue), `findByFingerprint(...)`.
59
+
60
+ ## Learning loop (#1889)
61
+
62
+ - **Persona instructions via prompt overrides** (`persona-prompt.ts`): a
63
+ persona's instructions apply through a per-persona registered prompt key
64
+ (`persona.<id>.instructions`, neutral empty base template) plus a
65
+ tenant-scoped `prompt_override`. Two personas of the same tenant/class resolve
66
+ different instructions without colliding on the override `(key, context)`
67
+ identity. `ensurePersonaInstructionsPrompt` (idempotent registration,
68
+ `editable` configurable), `applyPersonaInstructions` /
69
+ `upsertPromptTemplateOverride` (write the override — flows through
70
+ `PromptOverride.save()` → `validatePromptOverride`, so a non-editable template
71
+ is refused), `resolvePersonaInstructions`.
72
+ - **Memory scoping** (`persona-memory.ts`): `personaLearningMemory` routes a
73
+ `LearningMemory` by the persona's `memoryScope` (used as `owner_id`), so
74
+ personas learn independently. `reinforceFromFeedback` applies a `Feedback`
75
+ signal to memory as automatic, confidence-only reinforcement (ungated).
76
+ - **ReflectionRunner** (`reflection-runner.ts`): a schedulable pass that, under
77
+ the autonomous reflection principal (which **lacks**
78
+ `personas.activate-directive`), reinforces recent feedback, consolidates
79
+ episodes + feedback via an injected `reflect` boundary (the AI call, mocked in
80
+ tests), and records a **pending** `DirectiveProposal` (deduped by fingerprint).
81
+ It cannot activate anything.
82
+ - **The gate = a permission split** (`directive-principal.ts`,
83
+ `directive-approval.ts`): activating a directive is the permissioned operation
84
+ `personas.activate-directive` (contributed to the manifest permission catalog
85
+ via `registerPermissionDefinitions`). `DirectiveApprovalService.approve` /
86
+ `reject` first `assertCanActivate(principal)`; approval writes the
87
+ persona-scoped override and records an `accept` signal, rejection records a
88
+ `reject` signal and closes the proposal. A `DirectivePrincipal` is built from
89
+ granted slugs (`principalFromPermissions`) or a `PermissionResolver`
90
+ (`resolveDirectivePrincipal`).
91
+
92
+ ## Svelte UI
93
+
94
+ `@happyvertical/smrt-personas/svelte` ships a minimal, presentational
95
+ `DirectiveReviewQueue` component (Svelte 5): it lists pending proposals with a
96
+ current-vs-proposed diff and Approve/Reject actions, delegating to `onApprove`
97
+ (optionally with reviewer-edited text) / `onReject` callbacks a host wires to
98
+ the gated `DirectiveApprovalService`. Built with `svelte-package`; Svelte tests
99
+ run in CI only.
100
+
37
101
  ## Collection
38
102
 
39
103
  `AgentPersonaCollection`:
@@ -64,15 +128,51 @@ Because the walk reads personas across tenants, `resolve()` is intended for a
64
128
  system/admin path where cross-tenant reads are allowed — not inside a strict
65
129
  per-tenant interceptor context (same expectation as `resolveForTenant`).
66
130
 
131
+ ## Persona as Durable Instance (#1890)
132
+
133
+ A persona is a **durable instance** of an agent class — N per tenant, each
134
+ independently scheduled, memory-scoped, and permission-scoped. `persona-instance.ts`
135
+ wires that to the `@happyvertical/smrt-agents` multi-instance primitives (which are
136
+ per-package opt-in via `static multiInstance`, singleton by default):
137
+
138
+ - **Identity** — `personaInstanceKey(persona)` returns the persona id, or `null`
139
+ for the `default` persona (`DEFAULT_PERSONA_NAME`). A `null` key reuses the
140
+ **singleton** runtime identity (class-keyed dispatch subscriber + un-suffixed
141
+ memory scope). `agentOptionsForPersona(persona)` projects `{ instanceKey,
142
+ tenantId }` to spread into the agent constructor.
143
+ - **Scheduling** — `schedulePersonaInstance(schedules, persona, { cron, … })`
144
+ creates an `AgentSchedule` for the instance. The instance key rides in
145
+ `agentConfig` (which the scheduler spreads into the agent constructor →
146
+ `AgentOptions.instanceKey`); `agentId` is left **null** on purpose, because the
147
+ scheduler copies `agentId`→`SmrtJob.objectId` and the `TaskRunner`
148
+ `loadFromId()`s it against the agent's STI table — a persona id is not a row
149
+ there. The default persona carries no key → runs the singleton.
150
+ - **Admin** — `buildPersonaInstanceAdmin(personas, { tenantId, agentClass,
151
+ manifest })` renders the class's `uiSlots`/`adminRoutes` **once per instance**
152
+ (with the per-instance dispatch subscriber); `addPersonaInstance()` /
153
+ `removePersonaInstance(personaId)` are the add/remove affordances.
154
+ - **Non-destructive upgrade** — `upgradeSingletonToDefaultPersona(personas, {
155
+ tenantId, agentClass, runAsUserId, … })` maps an existing singleton to a single
156
+ `default` persona. It's null-keyed, so the running agent's dispatch subscriber,
157
+ memory, and config are untouched; idempotent (a second call returns the
158
+ existing default with `created: false` and never overwrites it).
159
+
67
160
  ## Dependencies
68
161
 
69
162
  Leaf package (acyclic by construction — nothing depends back on it):
70
163
 
71
164
  - Runtime: `@happyvertical/smrt-core`, `@happyvertical/smrt-tenancy`,
72
- `@happyvertical/smrt-agents` (the `TenantAgent` type it bridges).
165
+ `@happyvertical/smrt-agents` (the `TenantAgent` type it bridges, plus the
166
+ `AgentSchedule`/multi-instance surface `persona-instance.ts` builds on),
167
+ `@happyvertical/smrt-prompts` (persona instructions via `PromptOverride` +
168
+ `resolvePrompt`), `@happyvertical/smrt-users` (the permission catalog slug +
169
+ `PermissionResolver` principal), and `@happyvertical/sql` (`DatabaseInterface`
170
+ type). These edges are all acyclic (none of those packages depend on personas).
171
+ - Svelte peer + `@happyvertical/smrt-ui` for the optional `./svelte` review-queue
172
+ component.
73
173
  - `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`.
174
+ `@happyvertical/smrt-profiles` via `@crossPackageRef` string ids only. No
175
+ inter-`smrt-*` `peerDependencies`.
76
176
 
77
177
  ## Gotchas
78
178
 
@@ -83,3 +183,15 @@ Leaf package (acyclic by construction — nothing depends back on it):
83
183
  read through the helpers, so it never auto-hydrates to an object.
84
184
  - **Resolver runs outside strict tenant context** — it deliberately crosses
85
185
  tenants to walk the hierarchy.
186
+ - **The gate is the permission split, not a bespoke check** — the reflection
187
+ principal simply lacks `personas.activate-directive`, so it can only propose;
188
+ everything privileged in `DirectiveApprovalService` calls `assertCanActivate`
189
+ first. Instruction rewrites are gated; confidence-only reinforcement is not.
190
+ - **Persona instructions live in the override layer, not the base prompt** — the
191
+ per-persona prompt key registers a neutral empty template, so re-registration
192
+ stays idempotent as instructions change; the tenant-scoped override carries the
193
+ actual text. Activation still honours `validatePromptOverride` editability.
194
+ - **Directive dedup is by fingerprint across all statuses** — the runner skips a
195
+ proposal whose `(personaId, promptKey, proposedInstructions)` fingerprint
196
+ already exists (pending, approved, or rejected), so a rejected rewrite is never
197
+ re-surfaced.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=__smrt-register__.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}
@@ -0,0 +1,161 @@
1
+ import { SmrtCollection, SmrtObject } from '@happyvertical/smrt-core';
2
+ /**
3
+ * Return the canonical agent type identifier for storage and lookup.
4
+ *
5
+ * Uses the registry's qualified name when the class is registered and falls
6
+ * back to the raw input for dynamically defined or unregistered classes. This
7
+ * mirrors `getAgentTypeName()` in `@happyvertical/smrt-agents` without reaching
8
+ * into that package's internal module — personas store the same canonical form
9
+ * that `TenantAgent` does so the two layers line up.
10
+ */
11
+ export declare function canonicalAgentClass(name: string): string;
12
+ /**
13
+ * Return the meaningful aliases for an agent class — the canonical qualified
14
+ * name plus the simple class name — deduplicated.
15
+ *
16
+ * Lookups match against both so a persona persisted under a simple name (e.g.
17
+ * `Praeco`, when written through the generated create API without
18
+ * normalization) is still found when resolving under the canonical qualified
19
+ * name, and vice versa. Mirrors `getAgentTypeAliases()` in
20
+ * `@happyvertical/smrt-agents`.
21
+ */
22
+ export declare function agentClassAliases(name: string): string[];
23
+ /**
24
+ * AgentPersona SmrtObject — a tenant/context-scoped behavioral profile for an
25
+ * agent class.
26
+ */
27
+ export declare class AgentPersona extends SmrtObject {
28
+ /** Owning tenant (required — personas are never global). */
29
+ tenantId: string;
30
+ /**
31
+ * Canonical qualified agent type this persona configures
32
+ * (e.g. `@happyvertical/smrt-agents:Praeco`). Stored in canonical form so it
33
+ * lines up with `TenantAgent.agentClass`.
34
+ */
35
+ agentClass: string;
36
+ /**
37
+ * Human-readable persona name, unique per `(tenant, agentClass)`
38
+ * (see `conflictColumns`). Inherited `name` from {@link SmrtObject} is the
39
+ * conflict column; declared here for documentation and typing clarity.
40
+ */
41
+ name: string;
42
+ /**
43
+ * Optional context dimension type this persona is scoped to
44
+ * (e.g. `'site'`, `'property'`, `'chatRoom'`). `null` means the persona is a
45
+ * tenant-wide default that applies to every context for its agent class.
46
+ */
47
+ contextType: string | null;
48
+ /**
49
+ * Optional context dimension id. Only meaningful alongside `contextType`:
50
+ * when set, the persona targets one specific context row; when `null` (with
51
+ * a `contextType` set) it applies to every context of that type.
52
+ */
53
+ contextId: string | null;
54
+ /** System prompt / behavioral instructions layered onto the agent. */
55
+ instructions: string;
56
+ /**
57
+ * Allowed tool identifiers, stored as a JSON array string. Use
58
+ * {@link getAllowedTools} / {@link setAllowedTools} rather than reading this
59
+ * field directly. Empty array (`'[]'`) means "no persona-specific tools"
60
+ * (the resolver then falls back to manifest defaults, capped by the
61
+ * `TenantAgent` ceiling).
62
+ */
63
+ allowedTools: string;
64
+ /**
65
+ * The user whose principal this persona runs as (cross-package reference to
66
+ * `@happyvertical/smrt-users:User`). Required — a persona always runs as a
67
+ * concrete principal. Stored as a plain id column; no DDL FK constraint is
68
+ * emitted (cross-package).
69
+ */
70
+ runAsUserId: string;
71
+ /**
72
+ * Optional `Bot` profile the persona acts as for identity/audit
73
+ * (cross-package reference to `@happyvertical/smrt-profiles:Profile`).
74
+ * `null` when the persona has no distinct acting identity.
75
+ */
76
+ actsAsProfileId: string | null;
77
+ /**
78
+ * Memory scope key controlling how the agent's memory/reflection is
79
+ * partitioned for this persona. Empty string means "use a resolver-derived
80
+ * default".
81
+ */
82
+ memoryScope: string;
83
+ /**
84
+ * Resolution priority — higher wins when several personas apply to the same
85
+ * context at the same tenant level. Integer.
86
+ */
87
+ priority: number;
88
+ /** Whether this persona is active and eligible for resolution. */
89
+ enabled: boolean;
90
+ /**
91
+ * Allowed tool identifiers as an array.
92
+ *
93
+ * Tolerates malformed stored JSON by returning an empty array.
94
+ */
95
+ getAllowedTools(): string[];
96
+ /**
97
+ * Replace the allowed tool identifiers.
98
+ */
99
+ setAllowedTools(tools: string[]): void;
100
+ }
101
+ /**
102
+ * Collection for managing {@link AgentPersona} records.
103
+ */
104
+ export declare class AgentPersonaCollection extends SmrtCollection<AgentPersona> {
105
+ static readonly _itemClass: typeof AgentPersona;
106
+ /**
107
+ * List personas for a tenant + agent class, matching either the canonical
108
+ * qualified name or the simple class name (see {@link agentClassAliases}).
109
+ *
110
+ * Ordered by descending priority then name.
111
+ *
112
+ * @param extraWhere - Additional equality filters merged into the query
113
+ * (e.g. `{ enabled: true }`).
114
+ */
115
+ private listForClass;
116
+ /**
117
+ * List every persona bound to a tenant + agent class.
118
+ *
119
+ * Ordered by descending priority then name so callers that don't need the
120
+ * full resolver still get a deterministic, priority-first ordering.
121
+ */
122
+ byTenantAndClass(tenantId: string, agentClass: string): Promise<AgentPersona[]>;
123
+ /**
124
+ * List the enabled personas for a tenant + agent class, filtered to those
125
+ * applicable to a context.
126
+ *
127
+ * `enabled` is filtered in the query; the context predicate (tenant-wide vs
128
+ * type-scoped vs exact) is applied in memory since it is an OR over several
129
+ * columns. A persona is applicable when it is a tenant-wide default (no
130
+ * `contextType`) or its context matches the requested one. Ordered by
131
+ * descending priority then name.
132
+ *
133
+ * @param context - Optional `{ contextType, contextId }` to filter by.
134
+ */
135
+ findActive(tenantId: string, agentClass: string, context?: {
136
+ contextType?: string | null;
137
+ contextId?: string | null;
138
+ }): Promise<AgentPersona[]>;
139
+ }
140
+ /**
141
+ * Whether a persona applies to a requested context.
142
+ *
143
+ * - No `contextType` on the persona → tenant-wide default, applies to all.
144
+ * - `contextType` set, `contextId` null → type-scoped, applies when the
145
+ * requested `contextType` matches.
146
+ * - Both set → exact, applies when both match.
147
+ */
148
+ export declare function personaAppliesToContext(persona: Pick<AgentPersona, 'contextType' | 'contextId'>, context: {
149
+ contextType?: string | null;
150
+ contextId?: string | null;
151
+ }): boolean;
152
+ /**
153
+ * Context-specificity rank used for resolution ordering.
154
+ *
155
+ * - `2` exact `(contextType, contextId)` match
156
+ * - `1` type-scoped match (`contextType` matches, persona `contextId` null)
157
+ * - `0` tenant-wide default (persona has no `contextType`)
158
+ */
159
+ export declare function personaContextRank(persona: Pick<AgentPersona, 'contextType' | 'contextId'>): number;
160
+ export default AgentPersona;
161
+ //# sourceMappingURL=agent-persona.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-persona.d.ts","sourceRoot":"","sources":["../src/agent-persona.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAKL,cAAc,EACd,UAAU,EAEX,MAAM,0BAA0B,CAAC;AAGlC;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKxD;AAmBD;;;GAGG;AACH,qBAQa,YAAa,SAAQ,UAAU;IAC1C,4DAA4D;IAE5D,QAAQ,EAAE,MAAM,CAAM;IAEtB;;;;OAIG;IAEH,UAAU,EAAE,MAAM,CAAM;IAExB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAM;IAElB;;;;OAIG;IAEH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAElC;;;;OAIG;IAEH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEhC,sEAAsE;IAEtE,YAAY,EAAE,MAAM,CAAM;IAE1B;;;;;;OAMG;IAEH,YAAY,EAAE,MAAM,CAAQ;IAE5B;;;;;OAKG;IAEH,WAAW,EAAE,MAAM,CAAM;IAEzB;;;;OAIG;IAEH,eAAe,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEtC;;;;OAIG;IAEH,WAAW,EAAE,MAAM,CAAM;IAEzB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAK;IAErB,kEAAkE;IAElE,OAAO,EAAE,OAAO,CAAQ;IAExB;;;;OAIG;IACH,eAAe,IAAI,MAAM,EAAE;IAI3B;;OAEG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;CAGvC;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,cAAc,CAAC,YAAY,CAAC;IACtE,MAAM,CAAC,QAAQ,CAAC,UAAU,sBAAgB;IAE1C;;;;;;;;OAQG;YACW,YAAY;IAc1B;;;;;OAKG;IACG,gBAAgB,CACpB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IAI1B;;;;;;;;;;;OAWG;IACG,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAO,GACvE,OAAO,CAAC,YAAY,EAAE,CAAC;CAQ3B;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,WAAW,CAAC,EACxD,OAAO,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAClE,OAAO,CAWT;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,WAAW,CAAC,GACvD,MAAM,CAKR;AAWD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,94 @@
1
+ import { PromptOverride } from '@happyvertical/smrt-prompts';
2
+ import { DatabaseInterface } from '@happyvertical/sql';
3
+ import { AgentPersonaCollection } from './agent-persona.js';
4
+ import { DirectivePrincipal } from './directive-principal.js';
5
+ import { DirectiveProposal, DirectiveProposalCollection } from './directive-proposal.js';
6
+ import { Feedback, FeedbackCollection } from './feedback.js';
7
+ /** Raised when a principal without the activation permission attempts to review. */
8
+ export declare class DirectiveActivationDeniedError extends Error {
9
+ constructor(message?: string);
10
+ }
11
+ /** Raised when a review is attempted on a proposal that is not pending. */
12
+ export declare class DirectiveNotPendingError extends Error {
13
+ constructor(status: string);
14
+ }
15
+ /** Options for {@link DirectiveApprovalService}. */
16
+ export interface DirectiveApprovalServiceOptions {
17
+ db: DatabaseInterface;
18
+ proposals?: DirectiveProposalCollection;
19
+ feedback?: FeedbackCollection;
20
+ personas?: AgentPersonaCollection;
21
+ }
22
+ /** Result of approving a proposal. */
23
+ export interface DirectiveApprovalResult {
24
+ proposal: DirectiveProposal;
25
+ /** The activated persona-scoped override. */
26
+ override: PromptOverride;
27
+ /** The recorded `accept` signal. */
28
+ signal: Feedback;
29
+ }
30
+ /** Result of rejecting a proposal. */
31
+ export interface DirectiveRejectionResult {
32
+ proposal: DirectiveProposal;
33
+ /** The recorded `reject` signal. */
34
+ signal: Feedback;
35
+ }
36
+ /** A proposal reference: the row, or its id. */
37
+ export type ProposalRef = DirectiveProposal | string;
38
+ /**
39
+ * The permission-gated review surface for persona directives.
40
+ */
41
+ export declare class DirectiveApprovalService {
42
+ private readonly db;
43
+ private proposals?;
44
+ private feedback?;
45
+ private personas?;
46
+ constructor(options: DirectiveApprovalServiceOptions);
47
+ /**
48
+ * Assert a principal is authorised to review directives. The single gate:
49
+ * everything privileged in this service calls it first.
50
+ *
51
+ * @throws {DirectiveActivationDeniedError} when the principal lacks the slug.
52
+ */
53
+ assertCanActivate(principal: DirectivePrincipal): void;
54
+ /**
55
+ * Assert a principal may review a specific proposal: its resolved tenant, when
56
+ * bound, must match the proposal's tenant. Permissions are resolved per
57
+ * tenant, so holding the slug in one tenant must not authorise activating
58
+ * another tenant's directive.
59
+ *
60
+ * @throws {DirectiveActivationDeniedError} on a cross-tenant attempt.
61
+ */
62
+ private assertTenantMatch;
63
+ /**
64
+ * Approve a pending proposal: activate the persona-scoped override and record
65
+ * an `accept` signal. Pass `editedInstructions` to activate a revised text
66
+ * (the proposal keeps the model's original as its audit record; the override
67
+ * holds what was activated).
68
+ *
69
+ * @throws {DirectiveActivationDeniedError} when the principal lacks the slug.
70
+ * @throws {DirectiveNotPendingError} when the proposal is already reviewed.
71
+ */
72
+ approve(ref: ProposalRef, principal: DirectivePrincipal, options?: {
73
+ note?: string;
74
+ editedInstructions?: string;
75
+ }): Promise<DirectiveApprovalResult>;
76
+ /**
77
+ * Reject a pending proposal: record a `reject` signal and close it so it is
78
+ * never re-surfaced.
79
+ *
80
+ * @throws {DirectiveActivationDeniedError} when the principal lacks the slug.
81
+ * @throws {DirectiveNotPendingError} when the proposal is already reviewed.
82
+ */
83
+ reject(ref: ProposalRef, principal: DirectivePrincipal, options?: {
84
+ note?: string;
85
+ }): Promise<DirectiveRejectionResult>;
86
+ private loadPending;
87
+ private getProposal;
88
+ private recordSignal;
89
+ private syncPersonaInstructions;
90
+ private personasCollection;
91
+ private proposalsCollection;
92
+ private feedbackCollection;
93
+ }
94
+ //# sourceMappingURL=directive-approval.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directive-approval.d.ts","sourceRoot":"","sources":["../src/directive-approval.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,KAAK,QAAQ,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGlE,oFAAoF;AACpF,qBAAa,8BAA+B,SAAQ,KAAK;gBAErD,OAAO,SAA4G;CAKtH;AAED,2EAA2E;AAC3E,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,MAAM,EAAE,MAAM;CAI3B;AAED,oDAAoD;AACpD,MAAM,WAAW,+BAA+B;IAC9C,EAAE,EAAE,iBAAiB,CAAC;IACtB,SAAS,CAAC,EAAE,2BAA2B,CAAC;IACxC,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACnC;AAED,sCAAsC;AACtC,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,6CAA6C;IAC7C,QAAQ,EAAE,cAAc,CAAC;IACzB,oCAAoC;IACpC,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,sCAAsC;AACtC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,oCAAoC;IACpC,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,MAAM,CAAC;AAErD;;GAEG;AACH,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,OAAO,CAAC,SAAS,CAAC,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,CAAqB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAAyB;gBAE9B,OAAO,EAAE,+BAA+B;IAOpD;;;;;OAKG;IACH,iBAAiB,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI;IAMtD;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;OAQG;IACG,OAAO,CACX,GAAG,EAAE,WAAW,EAChB,SAAS,EAAE,kBAAkB,EAC7B,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC,uBAAuB,CAAC;IAwCnC;;;;;;OAMG;IACG,MAAM,CACV,GAAG,EAAE,WAAW,EAChB,SAAS,EAAE,kBAAkB,EAC7B,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAC9B,OAAO,CAAC,wBAAwB,CAAC;YAkBtB,WAAW;YAYX,WAAW;YAKX,YAAY;YAyBZ,uBAAuB;YAavB,kBAAkB;YAOlB,mBAAmB;YASnB,kBAAkB;CAMjC"}
@@ -0,0 +1,56 @@
1
+ import { PermissionDefinition, PermissionResolver } from '@happyvertical/smrt-users';
2
+ /** The permission slug that authorises activating a persona directive. */
3
+ export declare const ACTIVATE_DIRECTIVE_PERMISSION = "personas.activate-directive";
4
+ /** Catalog definition contributed for {@link ACTIVATE_DIRECTIVE_PERMISSION}. */
5
+ export declare const DIRECTIVE_ACTIVATION_PERMISSION_DEF: PermissionDefinition;
6
+ /**
7
+ * An actor whose authority is expressed as a set of held permission slugs.
8
+ *
9
+ * Deliberately minimal so the gate depends only on the permission model, not on
10
+ * any particular resolver or session machinery.
11
+ */
12
+ export interface DirectivePrincipal {
13
+ /** Optional stable id (recorded as the reviewer on approve/reject). */
14
+ readonly id?: string;
15
+ /**
16
+ * The tenant this principal's authority was resolved for. Permissions are
17
+ * resolved per tenant (`PermissionResolver.resolvePermissions(userId, tenantId)`),
18
+ * so a principal authorised in one tenant must not activate another tenant's
19
+ * directive. When set, the approval service enforces
20
+ * `principal.tenantId === proposal.tenantId`. `undefined` skips the check
21
+ * (e.g. a system/global actor or a hand-built principal).
22
+ */
23
+ readonly tenantId?: string;
24
+ /** Whether this actor holds the given permission slug. */
25
+ can(slug: string): boolean;
26
+ }
27
+ /**
28
+ * Build a {@link DirectivePrincipal} from an explicit set of granted slugs.
29
+ */
30
+ export declare function principalFromPermissions(permissions: Iterable<string>, options?: {
31
+ id?: string;
32
+ tenantId?: string;
33
+ }): DirectivePrincipal;
34
+ /**
35
+ * Build a {@link DirectivePrincipal} for a user in a tenant by resolving their
36
+ * effective permissions through the RBAC {@link PermissionResolver} — the
37
+ * production wiring that makes the gate concretely the permission system.
38
+ */
39
+ export declare function resolveDirectivePrincipal(options: {
40
+ resolver: PermissionResolver;
41
+ userId: string;
42
+ tenantId: string;
43
+ }): Promise<DirectivePrincipal>;
44
+ /**
45
+ * Register the persona directive-activation permission into the runtime catalog.
46
+ *
47
+ * @returns An unregister function (primarily for tests).
48
+ */
49
+ export declare function registerPersonaPermissions(): () => void;
50
+ /**
51
+ * Register the persona permissions once per process. Called as a side effect
52
+ * from the package entry so the slug is present in the permission catalog for
53
+ * any consumer that imports `@happyvertical/smrt-personas`.
54
+ */
55
+ export declare function ensurePersonaPermissionsRegistered(): void;
56
+ //# sourceMappingURL=directive-principal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directive-principal.d.ts","sourceRoot":"","sources":["../src/directive-principal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EAExB,MAAM,2BAA2B,CAAC;AAEnC,0EAA0E;AAC1E,eAAO,MAAM,6BAA6B,gCAAgC,CAAC;AAE3E,gFAAgF;AAChF,eAAO,MAAM,mCAAmC,EAAE,oBAMjD,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,0DAA0D;IAC1D,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC7B,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/C,kBAAkB,CAOpB;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,OAAO,EAAE;IACvD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAW9B;AAID;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,IAAI,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,IAAI,IAAI,CAMzD"}
@@ -0,0 +1,103 @@
1
+ import { SmrtCollection, SmrtObject } from '@happyvertical/smrt-core';
2
+ /**
3
+ * Lifecycle status of a proposal:
4
+ *
5
+ * - `pending` — awaiting review; surfaced in the queue.
6
+ * - `approved` — a reviewer activated it; the override was written.
7
+ * - `rejected` — a reviewer declined it; recorded as a signal and never
8
+ * re-surfaced (the runner dedups against it by fingerprint).
9
+ * - `superseded` — a newer proposal replaced it before review.
10
+ */
11
+ export type DirectiveProposalStatus = 'pending' | 'approved' | 'rejected' | 'superseded';
12
+ /**
13
+ * Structured evidence backing a proposal — the ids of the learning episodes and
14
+ * feedback signals the reflection consolidated. Pure references so the proposal
15
+ * stays auditable without duplicating payloads.
16
+ */
17
+ export interface DirectiveEvidence {
18
+ /** `_smrt_contexts` row ids of the episodes considered. */
19
+ episodeIds?: string[];
20
+ /** {@link Feedback} row ids considered. */
21
+ feedbackIds?: string[];
22
+ /** Free-form supporting notes (e.g. counts, aggregate metrics). */
23
+ notes?: string[];
24
+ [key: string]: unknown;
25
+ }
26
+ /**
27
+ * A stable content fingerprint for `(personaId, promptKey, proposedInstructions)`.
28
+ *
29
+ * Used to dedup proposals so an already-reviewed rewrite — in particular a
30
+ * rejected one — is never re-surfaced. Pure FNV-1a so it needs no crypto import
31
+ * and stays identical across Node and browser bundles.
32
+ */
33
+ export declare function computeDirectiveFingerprint(personaId: string, promptKey: string, proposedInstructions: string): string;
34
+ /**
35
+ * A pending, human-reviewable proposed edit to a persona's instructions.
36
+ */
37
+ export declare class DirectiveProposal extends SmrtObject {
38
+ /** Owning tenant (required — proposals always target a tenant's persona). */
39
+ tenantId: string;
40
+ /** The persona whose instructions this proposal would rewrite. */
41
+ personaId: string;
42
+ /** Canonical agent class the persona configures (denormalised for queries). */
43
+ agentClass: string;
44
+ /**
45
+ * The registered prompt key whose template the persona's instructions map to.
46
+ * Activation writes/updates the tenant-scoped override for this key.
47
+ */
48
+ promptKey: string;
49
+ /** The proposed replacement instructions (the new prompt template). */
50
+ proposedInstructions: string;
51
+ /** Snapshot of the instructions at proposal time (for a review diff). */
52
+ currentInstructions: string;
53
+ /** Model-authored explanation for the proposed change. */
54
+ rationale: string;
55
+ /** Structured evidence, stored as a JSON string (see {@link getEvidence}). */
56
+ evidence: string;
57
+ /** Lifecycle status. */
58
+ status: DirectiveProposalStatus;
59
+ /**
60
+ * Content fingerprint for dedup / not-re-surfacing. Set from
61
+ * {@link computeDirectiveFingerprint} at creation time.
62
+ */
63
+ fingerprint: string;
64
+ /** Aggregate confidence in the proposal, in `[0, 1]`. */
65
+ confidence: number;
66
+ /** Id of the principal (autonomous reflection actor) that proposed it. */
67
+ proposedBy: string;
68
+ /** The reviewer (user id) that approved/rejected it, once reviewed. */
69
+ reviewedBy: string | null;
70
+ /** When the proposal was reviewed. */
71
+ reviewedAt: Date | null;
72
+ /** Reviewer note attached on approve/reject. */
73
+ reviewNote: string | null;
74
+ /** The `prompt_override` id written on activation (`null` until approved). */
75
+ activatedOverrideId: string | null;
76
+ /** Parse {@link evidence}, tolerating malformed JSON. */
77
+ getEvidence(): DirectiveEvidence;
78
+ /** Replace {@link evidence}. */
79
+ setEvidence(value: DirectiveEvidence): void;
80
+ /** Whether the proposal is still awaiting review. */
81
+ isPending(): boolean;
82
+ }
83
+ /**
84
+ * Collection for {@link DirectiveProposal} rows — the review queue.
85
+ */
86
+ export declare class DirectiveProposalCollection extends SmrtCollection<DirectiveProposal> {
87
+ static readonly _itemClass: typeof DirectiveProposal;
88
+ /**
89
+ * The review queue: pending proposals, oldest first. Optionally scoped to a
90
+ * persona.
91
+ */
92
+ pending(options?: {
93
+ personaId?: string;
94
+ limit?: number;
95
+ }): Promise<DirectiveProposal[]>;
96
+ /**
97
+ * Existing proposals for a persona carrying a given fingerprint, in any
98
+ * status — used to avoid re-surfacing an already-seen (e.g. rejected) rewrite.
99
+ */
100
+ findByFingerprint(personaId: string, fingerprint: string): Promise<DirectiveProposal[]>;
101
+ }
102
+ export default DirectiveProposal;
103
+ //# sourceMappingURL=directive-proposal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directive-proposal.d.ts","sourceRoot":"","sources":["../src/directive-proposal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAGL,cAAc,EACd,UAAU,EAEX,MAAM,0BAA0B,CAAC;AAGlC;;;;;;;;GAQG;AACH,MAAM,MAAM,uBAAuB,GAC/B,SAAS,GACT,UAAU,GACV,UAAU,GACV,YAAY,CAAC;AAEjB;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,oBAAoB,EAAE,MAAM,GAC3B,MAAM,CASR;AAgBD;;GAEG;AACH,qBASa,iBAAkB,SAAQ,UAAU;IAC/C,6EAA6E;IAE7E,QAAQ,EAAE,MAAM,CAAM;IAEtB,kEAAkE;IAElE,SAAS,EAAE,MAAM,CAAM;IAEvB,+EAA+E;IAE/E,UAAU,EAAE,MAAM,CAAM;IAExB;;;OAGG;IAEH,SAAS,EAAE,MAAM,CAAM;IAEvB,uEAAuE;IAEvE,oBAAoB,EAAE,MAAM,CAAM;IAElC,yEAAyE;IAEzE,mBAAmB,EAAE,MAAM,CAAM;IAEjC,0DAA0D;IAE1D,SAAS,EAAE,MAAM,CAAM;IAEvB,8EAA8E;IAE9E,QAAQ,EAAE,MAAM,CAAQ;IAExB,wBAAwB;IAExB,MAAM,EAAE,uBAAuB,CAAa;IAE5C;;;OAGG;IAEH,WAAW,EAAE,MAAM,CAAM;IAEzB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAO;IAEzB,0EAA0E;IAE1E,UAAU,EAAE,MAAM,CAAM;IAExB,uEAAuE;IAEvE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEjC,sCAAsC;IAEtC,UAAU,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE/B,gDAAgD;IAEhD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEjC,8EAA8E;IAE9E,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE1C,yDAAyD;IACzD,WAAW,IAAI,iBAAiB;IAIhC,gCAAgC;IAChC,WAAW,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAI3C,qDAAqD;IACrD,SAAS,IAAI,OAAO;CAGrB;AAED;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,cAAc,CAAC,iBAAiB,CAAC;IAChF,MAAM,CAAC,QAAQ,CAAC,UAAU,2BAAqB;IAE/C;;;OAGG;IACG,OAAO,CACX,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GACnD,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAY/B;;;OAGG;IACG,iBAAiB,CACrB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAGhC;AAED,eAAe,iBAAiB,CAAC"}