@pikku/core 0.12.66 → 0.12.69

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 (36) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.js +1 -1
  4. package/dist/scopes.d.ts +14 -0
  5. package/dist/scopes.js +39 -8
  6. package/dist/services/in-memory-workflow-service.d.ts +2 -2
  7. package/dist/services/in-memory-workflow-service.js +2 -2
  8. package/dist/types/core.types.d.ts +22 -0
  9. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +18 -1
  10. package/dist/wirings/ai-agent/ai-agent-prepare.js +26 -4
  11. package/dist/wirings/queue/index.d.ts +1 -1
  12. package/dist/wirings/queue/queue.types.d.ts +30 -0
  13. package/dist/wirings/scope/validate-scope-definitions.d.ts +8 -0
  14. package/dist/wirings/scope/validate-scope-definitions.js +16 -1
  15. package/dist/wirings/workflow/index.d.ts +1 -1
  16. package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -3
  17. package/dist/wirings/workflow/pikku-workflow-service.js +86 -18
  18. package/dist/wirings/workflow/workflow.types.d.ts +38 -0
  19. package/package.json +1 -1
  20. package/src/index.ts +2 -1
  21. package/src/scopes.test.ts +37 -1
  22. package/src/scopes.ts +48 -9
  23. package/src/services/in-memory-workflow-service.test.ts +50 -1
  24. package/src/services/in-memory-workflow-service.ts +3 -2
  25. package/src/types/core.types.ts +23 -0
  26. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +29 -0
  27. package/src/wirings/ai-agent/ai-agent-prepare.ts +38 -4
  28. package/src/wirings/queue/index.ts +2 -0
  29. package/src/wirings/queue/queue.types.ts +32 -0
  30. package/src/wirings/scope/scope.test.ts +25 -0
  31. package/src/wirings/scope/validate-scope-definitions.ts +16 -1
  32. package/src/wirings/workflow/index.ts +1 -0
  33. package/src/wirings/workflow/pikku-workflow-service.test.ts +71 -0
  34. package/src/wirings/workflow/pikku-workflow-service.ts +110 -20
  35. package/src/wirings/workflow/workflow.types.ts +39 -0
  36. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,156 @@
1
+ ## 0.12.69
2
+
3
+ ### Patch Changes
4
+
5
+ - 24252b8: Emit queue meta for workflow-only projects, so per-workflow orchestrator queues actually work.
6
+
7
+ Workflows synthesise their own `wf-orchestrator-*` / `wf-step-*` queue meta during
8
+ post-processing, and those entries have no declaring source file. The queue codegen
9
+ bailed early on `queueWorkers.files.size === 0`, so a project that uses workflows but
10
+ hand-declares no `wireQueueWorker` wrote no queue meta at all — and the generated
11
+ bootstrap therefore never imported it.
12
+
13
+ With `queue.meta` empty at runtime, `getOrchestratorQueueName()` never found a
14
+ per-workflow queue and every workflow silently fell back to the single shared
15
+ `pikku-workflow-orchestrator` queue. Nothing failed, but the isolation was gone: one
16
+ long-running workflow step head-of-line-blocked every other workflow queued behind it.
17
+
18
+ The codegen now gates on the meta alone. `@pikku/core` additionally warns at wiring
19
+ time when workflows are registered but no per-workflow orchestrator queue is present,
20
+ so this degradation can't recur silently.
21
+
22
+ - e3d4454: Add job groups, so one shared queue can stay fair without splitting into one
23
+ queue per producer.
24
+
25
+ A job may now carry `group: { id, tier }`, and a worker may cap how many jobs
26
+ of any one group run at once via `groupConcurrency`. On pg-boss this maps to
27
+ `localGroupConcurrency`, which excludes at-capacity groups from the fetch query
28
+ itself, so a capped group costs nothing rather than being fetched and restored.
29
+ BullMQ declares it unsupported (groups are a BullMQ Pro feature) — being
30
+ push-based, it can simply use a queue per group at no polling cost.
31
+
32
+ Workflow services accept a `queueStrategy`. The default `'per-workflow'` is
33
+ unchanged: every workflow gets its own `wf-orchestrator-*` / `wf-step-*` queue,
34
+ which is also what lets serverless providers deploy one unit per workflow. The
35
+ new `'shared-groups'` routes every workflow through the shared
36
+ orchestrator/step-worker queues and isolates them by group instead, so a
37
+ monolith runs one set of pollers rather than one per workflow — on a
38
+ pull-based backend with dozens of workflows that is the difference between
39
+ hundreds of poll loops and twenty. It is for single-process runtimes only; a
40
+ per-unit serverless deploy still needs the per-workflow queues to route to its
41
+ units.
42
+
43
+ ## 0.12.68
44
+
45
+ ### Patch Changes
46
+
47
+ - f11675f: Forward the parent run's `context` into delegated sub-agent invocations.
48
+
49
+ A supervisor agent's injected `context` (the "Current context" block holding the
50
+ authoritative identifiers — organizationId, project/stage ids) was appended only
51
+ to the supervisor's own instructions. When it delegated, the sub-agent tool's
52
+ input schema carries just `{ message, session }`, and `buildToolDefs` invoked the
53
+ sub-agent with `{ message, threadId, resourceId }` — dropping the context. The
54
+ sub-agent therefore never saw the real ids and depended on the model re-typing
55
+ them into the free-text `message`, which weaker models routinely botch, producing
56
+ schema-validation and permission rejections that the agent then retries — burning
57
+ steps and ballooning the transcript.
58
+
59
+ `buildToolDefs` now takes the parent `context` and forwards it (via the new
60
+ `buildSubAgentRunInput` helper) into both the streaming and non-streaming
61
+ sub-agent invocations, so a specialist inherits the same identifier block in its
62
+ instructions.
63
+
64
+ ## 0.12.67
65
+
66
+ ### Patch Changes
67
+
68
+ - ae4f59a: Gate admin capabilities on scopes, and scaffold user management
69
+
70
+ Admin capabilities were gated on `user.role === 'admin'` — a single text column
71
+ meaning "can do everything". Impersonating a user, rebinding a shared
72
+ credential and reading the user directory are distinct capabilities that one
73
+ user can hold independently, so they are now scopes on an `admin` tree:
74
+
75
+ | Gate | Scope |
76
+ | -------------------------------------- | ------------------------ |
77
+ | impersonation | `admin:impersonate` |
78
+ | `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |
79
+ | reading the user directory | `admin:users:list` |
80
+ | creating a user out of band | `admin:users:create` |
81
+ | ban / unban | `admin:users:ban` |
82
+ | delete a user | `admin:users:remove` |
83
+ | revoke a user's sessions | `admin:users:sessions` |
84
+ | set a user's password | `admin:users:password` |
85
+
86
+ Holding the bare `admin` scope satisfies all of them via pikku's existing
87
+ parent-grant rule, so it is a one-for-one replacement for the old role.
88
+
89
+ better-auth's `admin()` plugin is still what implements ban, delete,
90
+ session-revocation and set-password, so it stays. Its `user.role` column is no
91
+ longer something pikku grants: it is _projected_ from the scope store when a
92
+ session is built, and only from the scopes whose capability better-auth's own
93
+ endpoints gate on the caller's role. Someone granted `admin:users:list` can read
94
+ the directory — which goes straight to the auth adapter — without gaining the
95
+ power to ban, and revoking a scope demotes the role on the next sign-in. Scopes
96
+ remain the single source of truth.
97
+
98
+ New `scaffold.userAdmin` in `pikku.config.json` generates the whole set —
99
+ `pikkuAdminListUsers`, `pikkuAdminCreateUser`, `pikkuAdminSetUserBanned`,
100
+ `pikkuAdminRemoveUser`, `pikkuAdminRevokeUserSessions` and
101
+ `pikkuAdminSetUserPassword` — into your project. Listing or banning a user is
102
+ ordinary application behaviour and must not require installing the console.
103
+ Codegen fails with an actionable error if better-auth is wired without
104
+ `admin()`. The console's Users page calls these same functions, showing each
105
+ action only where the caller holds its scope.
106
+
107
+ Every scaffold now emits a directory named for its domain — `scaffold/admin/`,
108
+ `scaffold/rpc/`, `scaffold/agent/`, `scaffold/auth/`, `scaffold/console/`,
109
+ `scaffold/graph/`, `scaffold/realtime/`, `scaffold/scenarios/`,
110
+ `scaffold/webhook/`, `scaffold/workflow/` — holding its wiring file beside a
111
+ `*.schemas.gen.ts` sibling, and every generated payload is a zod schema instead
112
+ of a TypeScript generic. The schemas have to stand alone: the inspector reads a
113
+ zod schema by importing the module that declares it, which it cannot do for a
114
+ wiring file whose relative pikku-types import per-unit deploy codegen rewrites.
115
+
116
+ Resolving a schema by reference rather than by name also fixes the agent HTTP
117
+ surface. `agentCaller` and `agentStreamCaller` take the same payload but had to
118
+ repeat the type literal verbatim in each generic position, because the extractor
119
+ synthesised the schema name from the _function_ name and so recorded an
120
+ `inputSchemaName` with no schema behind it whenever the two shared a named
121
+ alias — every agent call through that alias failed with `MissingSchemaError`.
122
+ One `AgentCall` schema now backs both.
123
+
124
+ Where a payload's shape belongs to `@pikku/core` (`WorkflowRunStatus`,
125
+ `FunctionCoverageReport`, `StubCall[]`) the generated function carries no
126
+ `output` schema and the inspector infers it from the handler's return type;
127
+ re-declaring a core type in zod would be a second definition free to drift.
128
+
129
+ Upgrading rewrites the layout in place: codegen prunes the pre-directory copy of
130
+ each scaffold file before it inspects the source tree, since the old flat file
131
+ still wires the same routes and leaving it behind would wire everything twice.
132
+
133
+ `@pikku/core` gains `hasScopes(required, held)`, the non-throwing counterpart to
134
+ `verifyScopes`, and declares `auth` on `CoreSingletonServices` — the auth
135
+ instance the generated `pikkuServices` wrapper already injected but never typed.
136
+ A scope root declared twice (an addon and its host both contributing the same
137
+ `admin` tree) now flattens to one entry per id instead of emitting it twice.
138
+
139
+ BREAKING: there is no role fallback for the scope-gated capabilities. An app
140
+ that relied on the old default must register a `ScopeService` and grant `admin`
141
+ (or a narrower `admin:*` scope). Every gate fails closed and warns when no
142
+ `ScopeService` is registered. `delegatedAuth`'s `defaultRole`/`mapRole` now
143
+ grant a pikku role through the `ScopeService` instead of writing better-auth's
144
+ `role` column, and the `credentialOAuth` platform user no longer sets `banned`.
145
+
146
+ BREAKING: the console reads its user directory over the scaffolded
147
+ `pikkuAdminListUsers` RPC (gated on `admin:users:list`, backed by better-auth's
148
+ `$context.adapter`) instead of `client.admin.listUsers`, and
149
+ `UsersTableUser`/`UsersTableLabels` no longer carry `role` — there is no role
150
+ column to render. `@pikku/addon-console` no longer ships a `console:listUsers`
151
+ function: user management is not the console's job, so a host that wants the
152
+ Users page must enable `scaffold.userAdmin`.
153
+
1
154
  ## 0.12.66
2
155
 
3
156
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module @pikku/core
3
3
  */
4
- export type { CommonWireMeta, CoreConfig, CorePikkuMiddleware, CorePikkuMiddlewareConfig, CorePikkuMiddlewareFactory, CorePikkuMiddlewareGroup, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, ServerLifecycle, FunctionMeta, FunctionRuntimeMeta, FunctionServicesMeta, FunctionWiresMeta, FunctionsMeta, FunctionsRuntimeMeta, JSONPrimitive, JSONValue, MakeRequired, MiddlewareMetadata, MiddlewarePriority, PermissionMetadata, PickOptional, PickRequired, PikkuAIMiddlewareHooks, PikkuWire, PikkuRawWire, PikkuWiringTypes, PostgresConfig, RequireAtLeastOne, SecurityAuditIssue, SecurityAuditReport, SecurityAuditSummary, SecurityAuditUpdate, SecuritySeverity, SecurityUpdateLevel, SerializedError, WireServices, } from './types/core.types.js';
4
+ export type { AuthInstance, CommonWireMeta, CoreConfig, CorePikkuMiddleware, CorePikkuMiddlewareConfig, CorePikkuMiddlewareFactory, CorePikkuMiddlewareGroup, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, ServerLifecycle, FunctionMeta, FunctionRuntimeMeta, FunctionServicesMeta, FunctionWiresMeta, FunctionsMeta, FunctionsRuntimeMeta, JSONPrimitive, JSONValue, MakeRequired, MiddlewareMetadata, MiddlewarePriority, PermissionMetadata, PickOptional, PickRequired, PikkuAIMiddlewareHooks, PikkuWire, PikkuRawWire, PikkuWiringTypes, PostgresConfig, RequireAtLeastOne, SecurityAuditIssue, SecurityAuditReport, SecurityAuditSummary, SecurityAuditUpdate, SecuritySeverity, SecurityUpdateLevel, SerializedError, WireServices, } from './types/core.types.js';
5
5
  export { pikkuAIMiddleware, pikkuChannelMiddleware, pikkuChannelMiddlewareFactory, pikkuMiddleware, pikkuMiddlewareFactory, } from './types/core.types.js';
6
6
  export type { CorePikkuAuth, CorePikkuAuthConfig, CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuPermission, CorePikkuPermissionConfig, CorePikkuPermissionFactory, CorePikkuApprovalDescription, CorePermissionGroup, ZodLike, } from './function/functions.types.js';
7
7
  export { pikkuAuth, pikkuPermission, pikkuPermissionFactory, pikkuApprovalDescription, } from './function/functions.types.js';
@@ -50,7 +50,7 @@ export type { WireRemoteAddonConfig, RemoteAddonAuth, } from './wirings/rpc/wire
50
50
  export type { PikkuPackageState } from './types/state.types.js';
51
51
  export { runMiddleware, addTagMiddleware, addTagMiddleware as addMiddleware, addGlobalMiddleware, } from './middleware-runner.js';
52
52
  export { addGlobalPermission, checkAuthPermissions } from './permissions.js';
53
- export { verifyScopes } from './scopes.js';
53
+ export { hasScopes, verifyScopes } from './scopes.js';
54
54
  export { isSerializable, stopSingletonServices, pikkuServerLifecycle, } from './utils.js';
55
55
  export { getSingletonServices, getCreateWireServices, setSingletonServices, } from './pikku-state.js';
56
56
  export { clearPikkuRuntimeState } from './test-utils.js';
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ export { wireAddon } from './wirings/rpc/wire-addon.js';
18
18
  export { wireRemoteAddon } from './wirings/rpc/wire-remote-addon.js';
19
19
  export { runMiddleware, addTagMiddleware, addTagMiddleware as addMiddleware, addGlobalMiddleware, } from './middleware-runner.js';
20
20
  export { addGlobalPermission, checkAuthPermissions } from './permissions.js';
21
- export { verifyScopes } from './scopes.js';
21
+ export { hasScopes, verifyScopes } from './scopes.js';
22
22
  export { isSerializable, stopSingletonServices, pikkuServerLifecycle, } from './utils.js';
23
23
  export { getSingletonServices, getCreateWireServices, setSingletonServices, } from './pikku-state.js';
24
24
  export { clearPikkuRuntimeState } from './test-utils.js';
package/dist/scopes.d.ts CHANGED
@@ -1,4 +1,18 @@
1
1
  import type { CoreUserSession } from './types/core.types.js';
2
+ /**
3
+ * Whether a set of held grants satisfies every required scope.
4
+ *
5
+ * The non-throwing counterpart to {@link verifyScopes}, for deciding rather
6
+ * than enforcing — an authorization gate that falls back to another check when
7
+ * it is not satisfied, rather than rejecting the request outright.
8
+ *
9
+ * Fails closed: an absent or empty `held` satisfies nothing. An empty
10
+ * `required` is satisfied by anything.
11
+ *
12
+ * @param required - Scopes to check for. Empty means no gate.
13
+ * @param held - The grants held, e.g. `session.scopes`. May be undefined.
14
+ */
15
+ export declare const hasScopes: (required: readonly string[] | undefined, held: Iterable<string> | undefined) => boolean;
2
16
  /**
3
17
  * Verifies that a session holds every required scope, throwing on the first
4
18
  * one it does not.
package/dist/scopes.js CHANGED
@@ -31,6 +31,42 @@ const satisfyingGrants = (scope) => {
31
31
  * `admin`.
32
32
  */
33
33
  const holds = (held, scope) => satisfyingGrants(scope).some((grant) => held.has(grant));
34
+ /**
35
+ * The first required scope a set of held grants does not satisfy, or `null`
36
+ * when every one is satisfied.
37
+ *
38
+ * Scopes are an AND gate: every entry in `required` must be satisfied. This is
39
+ * deliberately distinct from `permissions`, which OR together — a scope can
40
+ * only ever narrow access, so adding one to a function can never widen it.
41
+ *
42
+ * Fails closed: an absent `held` satisfies nothing.
43
+ */
44
+ const firstUnsatisfied = (required, held) => {
45
+ if (!required || required.length === 0) {
46
+ return null;
47
+ }
48
+ const grants = new Set(held ?? []);
49
+ for (const scope of required) {
50
+ if (!holds(grants, scope)) {
51
+ return scope;
52
+ }
53
+ }
54
+ return null;
55
+ };
56
+ /**
57
+ * Whether a set of held grants satisfies every required scope.
58
+ *
59
+ * The non-throwing counterpart to {@link verifyScopes}, for deciding rather
60
+ * than enforcing — an authorization gate that falls back to another check when
61
+ * it is not satisfied, rather than rejecting the request outright.
62
+ *
63
+ * Fails closed: an absent or empty `held` satisfies nothing. An empty
64
+ * `required` is satisfied by anything.
65
+ *
66
+ * @param required - Scopes to check for. Empty means no gate.
67
+ * @param held - The grants held, e.g. `session.scopes`. May be undefined.
68
+ */
69
+ export const hasScopes = (required, held) => firstUnsatisfied(required, held) === null;
34
70
  /**
35
71
  * Verifies that a session holds every required scope, throwing on the first
36
72
  * one it does not.
@@ -47,13 +83,8 @@ const holds = (held, scope) => satisfyingGrants(scope).some((grant) => held.has(
47
83
  * @throws {MissingScopeError} Naming the first unsatisfied scope.
48
84
  */
49
85
  export const verifyScopes = (required, session) => {
50
- if (!required || required.length === 0) {
51
- return;
52
- }
53
- const held = new Set(session?.scopes ?? []);
54
- for (const scope of required) {
55
- if (!holds(held, scope)) {
56
- throw new MissingScopeError(scope);
57
- }
86
+ const missing = firstUnsatisfied(required, session?.scopes);
87
+ if (missing !== null) {
88
+ throw new MissingScopeError(missing);
58
89
  }
59
90
  };
@@ -1,6 +1,6 @@
1
1
  import { PikkuWorkflowService } from '../wirings/workflow/pikku-workflow-service.js';
2
2
  import type { SerializedError } from '../types/core.types.js';
3
- import type { WorkflowPlannedStep, WorkflowRun, WorkflowRunService, WorkflowRunWire, StepState, StepStatus, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from '../wirings/workflow/workflow.types.js';
3
+ import type { WorkflowPlannedStep, WorkflowQueueOptions, WorkflowRun, WorkflowRunService, WorkflowRunWire, StepState, StepStatus, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from '../wirings/workflow/workflow.types.js';
4
4
  /**
5
5
  * In-memory implementation of WorkflowService for inline-only execution
6
6
  *
@@ -17,7 +17,7 @@ import type { WorkflowPlannedStep, WorkflowRun, WorkflowRunService, WorkflowRunW
17
17
  * ```
18
18
  */
19
19
  export declare class InMemoryWorkflowService extends PikkuWorkflowService implements WorkflowRunService {
20
- constructor();
20
+ constructor(options?: WorkflowQueueOptions);
21
21
  private sleepTimers;
22
22
  private runs;
23
23
  private steps;
@@ -17,8 +17,8 @@ import { isExpectedError } from '../errors/error-handler.js';
17
17
  * ```
18
18
  */
19
19
  export class InMemoryWorkflowService extends PikkuWorkflowService {
20
- constructor() {
21
- super({ wireQueues: false });
20
+ constructor(options = {}) {
21
+ super({ ...options, wireQueues: false });
22
22
  }
23
23
  sleepTimers = new Set();
24
24
  runs = new Map();
@@ -209,6 +209,21 @@ export interface CoreUserSession {
209
209
  */
210
210
  scopes?: string[];
211
211
  }
212
+ /**
213
+ * The shape pikku needs from whatever auth library a project wires: something
214
+ * that can answer an HTTP request and expose its own endpoints as callable
215
+ * methods. Kept structural so core stays independent of any one auth package —
216
+ * `@pikku/better-auth`'s `BetterAuthInstance` is this type.
217
+ */
218
+ export interface AuthInstance {
219
+ handler: (request: Request) => Promise<Response>;
220
+ api: Record<string, any>;
221
+ /**
222
+ * The auth library's resolved context. Optional because a hand-built instance
223
+ * may omit it.
224
+ */
225
+ $context?: Promise<any>;
226
+ }
212
227
  /**
213
228
  * Interface for core singleton services provided by Pikku.
214
229
  */
@@ -280,6 +295,13 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
280
295
  * better-auth's `mapSession`), never by the function runner.
281
296
  */
282
297
  scopeService?: ScopeService;
298
+ /**
299
+ * The project's resolved auth instance, built once by the factory an auth
300
+ * package registers (e.g. `pikkuBetterAuth`) and injected by the generated
301
+ * `pikkuServices` wrapper — which is why service factories are forbidden from
302
+ * returning it themselves. Absent when the project wires no auth.
303
+ */
304
+ auth?: () => Promise<AuthInstance>;
283
305
  }
284
306
  /**
285
307
  * Represents different forms of wire within Pikku and the outside world.
@@ -168,7 +168,24 @@ export type ScopedChannel = AIStreamChannel & {
168
168
  }>;
169
169
  };
170
170
  export declare function createScopedChannel(parent: AIStreamChannel, agentName: string, session: string): ScopedChannel;
171
- export declare function buildToolDefs(params: RunAIAgentParams, agentSessionMap: Map<string, string>, resourceId: string, agentName: string, packageName: string | null, streamContext?: StreamContext, aiMiddlewares?: PikkuAIMiddlewareHooks[], agentMode?: 'delegate' | 'supervise'): Promise<{
171
+ /**
172
+ * Build the run input for a delegated sub-agent.
173
+ *
174
+ * `context` is the PARENT run's identifier block (the "Current context" text
175
+ * with organizationId, project/stage ids). A sub-agent's tool-call schema only
176
+ * carries { message, session }, so unless the sub-agent inherits the parent's
177
+ * context it never sees the authoritative ids — it depends on the model
178
+ * re-typing them into `message`, which weak models botch, causing
179
+ * schema/permission rejections and retry loops. Forwarding it here is the
180
+ * regression this seam guards.
181
+ */
182
+ export declare function buildSubAgentRunInput(message: string, threadId: string, resourceId: string, parentContext?: string): {
183
+ message: string;
184
+ threadId: string;
185
+ resourceId: string;
186
+ context?: string;
187
+ };
188
+ export declare function buildToolDefs(params: RunAIAgentParams, agentSessionMap: Map<string, string>, resourceId: string, agentName: string, packageName: string | null, streamContext?: StreamContext, aiMiddlewares?: PikkuAIMiddlewareHooks[], agentMode?: 'delegate' | 'supervise', parentContext?: string): Promise<{
172
189
  tools: AIAgentToolDef[];
173
190
  missingRpcs: string[];
174
191
  }>;
@@ -325,7 +325,29 @@ export function createScopedChannel(parent, agentName, session) {
325
325
  clearState: () => parent.clearState(),
326
326
  };
327
327
  }
328
- export async function buildToolDefs(params, agentSessionMap, resourceId, agentName, packageName, streamContext, aiMiddlewares, agentMode) {
328
+ /**
329
+ * Build the run input for a delegated sub-agent.
330
+ *
331
+ * `context` is the PARENT run's identifier block (the "Current context" text
332
+ * with organizationId, project/stage ids). A sub-agent's tool-call schema only
333
+ * carries { message, session }, so unless the sub-agent inherits the parent's
334
+ * context it never sees the authoritative ids — it depends on the model
335
+ * re-typing them into `message`, which weak models botch, causing
336
+ * schema/permission rejections and retry loops. Forwarding it here is the
337
+ * regression this seam guards.
338
+ */
339
+ export function buildSubAgentRunInput(message, threadId, resourceId, parentContext) {
340
+ return { message, threadId, resourceId, context: parentContext };
341
+ }
342
+ export async function buildToolDefs(params, agentSessionMap, resourceId, agentName, packageName, streamContext, aiMiddlewares, agentMode,
343
+ // The parent run's `context` (the "Current context" identifier block). A
344
+ // delegated sub-agent's tool-call input schema only carries { message,
345
+ // session }, so without inheriting this the sub-agent never sees the
346
+ // authoritative ids (organizationId, project/stage ids) — it depends on the
347
+ // model re-typing them into `message`, which weak models botch, causing
348
+ // schema/permission rejections and retry loops. Forward it so the sub-agent
349
+ // gets the same context block in its instructions.
350
+ parentContext) {
329
351
  const singletonServices = getSingletonServices();
330
352
  const tools = [];
331
353
  const missingRpcs = [];
@@ -505,7 +527,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
505
527
  subChannel.send(event);
506
528
  },
507
529
  };
508
- const resultText = await streamAIAgent(subAgentName, { message, threadId, resourceId }, effectiveChannel, params, agentSessionMap, streamContext.options);
530
+ const resultText = await streamAIAgent(subAgentName, buildSubAgentRunInput(message, threadId, resourceId, parentContext), effectiveChannel, params, agentSessionMap, streamContext.options);
509
531
  if (subChannel.approvals.length > 0) {
510
532
  return {
511
533
  [APPROVAL_REQUIRED]: true,
@@ -525,7 +547,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
525
547
  return resultText;
526
548
  }
527
549
  // No stream context: sub-agent runs non-streaming
528
- const result = await runAIAgent(subAgentName, { message, threadId, resourceId }, params, agentSessionMap);
550
+ const result = await runAIAgent(subAgentName, buildSubAgentRunInput(message, threadId, resourceId, parentContext), params, agentSessionMap);
529
551
  if (result.status === 'suspended' &&
530
552
  result.pendingApprovals?.length) {
531
553
  return {
@@ -731,7 +753,7 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
731
753
  const allMessages = [...contextMessages, ...messages, userMessage];
732
754
  const trimmedMessages = trimMessages(allMessages);
733
755
  const aiMiddlewares = agent.aiMiddleware ?? [];
734
- const { tools, missingRpcs } = await buildToolDefs(params, agentSessionMap, input.resourceId, resolvedName, packageName, streamContext, aiMiddlewares, agent.agentMode);
756
+ const { tools, missingRpcs } = await buildToolDefs(params, agentSessionMap, input.resourceId, resolvedName, packageName, streamContext, aiMiddlewares, agent.agentMode, input.context);
735
757
  let instructions = await buildInstructions(resolvedName, packageName);
736
758
  if (input.context) {
737
759
  instructions = `${instructions}\n\nCurrent context (use these identifiers directly in tool calls — do not ask the user for them):\n${input.context}`;
@@ -1,4 +1,4 @@
1
- export type { ConfigValidationResult, CoreQueueWorker, JobOptions, PikkuJobConfig, PikkuWorkerConfig, PikkuQueue, QueueJob, QueueJobStatus, QueueService, QueueWorkers, QueueWorkersMeta, } from './queue.types.js';
1
+ export type { ConfigValidationResult, CoreQueueWorker, GroupConcurrencyConfig, JobGroup, JobOptions, PikkuJobConfig, PikkuWorkerConfig, PikkuQueue, QueueJob, QueueJobStatus, QueueService, QueueWorkers, QueueWorkersMeta, } from './queue.types.js';
2
2
  export { wireQueueWorker, runQueueJob, getQueueWorkers, removeQueueWorker, QueueJobDiscardedError, QueueJobFailedError, } from './queue-runner.js';
3
3
  export { validateWorkerConfig } from './validate-worker-config.js';
4
4
  export type { QueueConfigMapping } from './validate-worker-config.js';
@@ -30,6 +30,34 @@ export interface PikkuWorkerConfig {
30
30
  maxStalledCount?: number;
31
31
  /** Condition to start processor at instance creation */
32
32
  autorun?: boolean;
33
+ /**
34
+ * Cap how many jobs of any one group ({@link JobOptions.group}) may run at
35
+ * once, so a single group can't occupy the whole worker. Lets one shared
36
+ * queue stay fair across producers instead of splitting it into one queue
37
+ * per producer — which multiplies polling cost on pull-based backends.
38
+ * Must not exceed {@link batchSize}.
39
+ */
40
+ groupConcurrency?: number | GroupConcurrencyConfig;
41
+ }
42
+ /**
43
+ * Per-group concurrency limits, optionally varied by tier so slow groups can
44
+ * be allowed more (or fewer) slots than the default.
45
+ */
46
+ export interface GroupConcurrencyConfig {
47
+ /** Limit applied to any group without a matching tier */
48
+ default: number;
49
+ /** Per-tier overrides, keyed by {@link JobGroup.tier} */
50
+ tiers?: Record<string, number>;
51
+ }
52
+ /**
53
+ * Fairness key for a job. Jobs sharing an `id` count against the same
54
+ * {@link PikkuWorkerConfig.groupConcurrency} limit.
55
+ */
56
+ export interface JobGroup {
57
+ /** Group this job belongs to (e.g. a workflow name) */
58
+ id: string;
59
+ /** Optional tier selecting a per-tier limit */
60
+ tier?: string;
33
61
  }
34
62
  /**
35
63
  * Configuration for individual jobs - how jobs behave
@@ -106,6 +134,8 @@ export interface JobOptions {
106
134
  jobId?: string;
107
135
  /** Pikku user ID to propagate to the queue worker for credential resolution */
108
136
  pikkuUserId?: string;
137
+ /** Fairness key — counts against the worker's {@link PikkuWorkerConfig.groupConcurrency} */
138
+ group?: JobGroup;
109
139
  }
110
140
  /**
111
141
  * Queue provider interface for job publishing operations
@@ -3,6 +3,14 @@ import type { FlatScope, ScopeDefinitions, ScopeDefinitionsMeta } from './scope.
3
3
  * Flattens declared scope trees into the full list of grantable scope ids,
4
4
  * depth-first. Every node is emitted, including intermediate ones.
5
5
  *
6
+ * Ids are unique. A root may legitimately be declared more than once — an addon
7
+ * and its host app both contributing the same `admin` tree, say — and
8
+ * {@link validateAndBuildScopeDefinitionsMeta} already guarantees those
9
+ * declarations are identical, so the second one is redundant rather than
10
+ * conflicting. Collapsing it here keeps every consumer honest: codegen emits an
11
+ * object literal keyed by id (duplicates are a TypeScript error), and a
12
+ * ScopeService syncs one row per scope instead of re-writing the same one.
13
+ *
6
14
  * Used by codegen to build the `ScopeId` union, and by a ScopeService to sync
7
15
  * the declared set into its store.
8
16
  */
@@ -30,6 +30,14 @@ const flattenNodes = (nodes, prefix, out) => {
30
30
  * Flattens declared scope trees into the full list of grantable scope ids,
31
31
  * depth-first. Every node is emitted, including intermediate ones.
32
32
  *
33
+ * Ids are unique. A root may legitimately be declared more than once — an addon
34
+ * and its host app both contributing the same `admin` tree, say — and
35
+ * {@link validateAndBuildScopeDefinitionsMeta} already guarantees those
36
+ * declarations are identical, so the second one is redundant rather than
37
+ * conflicting. Collapsing it here keeps every consumer honest: codegen emits an
38
+ * object literal keyed by id (duplicates are a TypeScript error), and a
39
+ * ScopeService syncs one row per scope instead of re-writing the same one.
40
+ *
33
41
  * Used by codegen to build the `ScopeId` union, and by a ScopeService to sync
34
42
  * the declared set into its store.
35
43
  */
@@ -39,7 +47,14 @@ export const flattenScopeDefinitions = (definitions) => {
39
47
  out.push({ id: def.name, description: def.description });
40
48
  flattenNodes(def.scopes, def.name, out);
41
49
  }
42
- return out;
50
+ const seen = new Set();
51
+ return out.filter((scope) => {
52
+ if (seen.has(scope.id)) {
53
+ return false;
54
+ }
55
+ seen.add(scope.id);
56
+ return true;
57
+ });
43
58
  };
44
59
  /**
45
60
  * Validates declared scopes and keys them by name.
@@ -11,5 +11,5 @@ export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGr
11
11
  export { validateWorkflowWiring, computeEntryNodeIds, } from './graph/graph-validation.js';
12
12
  export { pikkuWorkflowWorkerFunc, pikkuWorkflowOrchestratorFunc, pikkuWorkflowSleeperFunc, } from './workflow-queue-workers.js';
13
13
  export type { WorkflowStepInput as WorkflowStepQueueInput, PikkuWorkflowOrchestratorInput, PikkuWorkflowSleeperInput, } from './workflow-queue-workers.js';
14
- export type { WorkflowService, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
14
+ export type { WorkflowService, WorkflowQueueOptions, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
15
15
  export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowWireDoInline, WorkflowWireSleep, WorkflowWireSuspend, WorkflowWireApproval, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, SimpleCondition, Condition, BranchCase, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, PikkuScenarioWire, } from './workflow.types.js';
@@ -1,10 +1,10 @@
1
1
  import type { SerializedError } from '../../types/core.types.js';
2
- import type { ApprovalOutcome, PikkuScenarioWire, StepState, StepStatus, WorkflowPlannedStep, WorkflowRun, WorkflowRunMirror, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
2
+ import type { ApprovalOutcome, PikkuScenarioWire, StepState, StepStatus, WorkflowPlannedStep, WorkflowRun, WorkflowRunMirror, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowQueueOptions, WorkflowStepOptions } from './workflow.types.js';
3
3
  import type { WorkflowService } from '../../services/workflow-service.js';
4
4
  import type { ScenarioActors } from '../../services/scenario-actors-service.js';
5
5
  import { PikkuError } from '../../errors/error-handler.js';
6
6
  import { type RunTimeline, type ReconstructedRunState } from './run-timeline.js';
7
- import type { JobOptions } from '../queue/queue.types.js';
7
+ import type { GroupConcurrencyConfig, JobGroup, JobOptions } from '../queue/queue.types.js';
8
8
  /**
9
9
  * Default number of retries for a workflow step when none is specified. The
10
10
  * workflow — not the queue — owns retry policy; a step inherits this unless it
@@ -98,10 +98,13 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
98
98
  private runActors;
99
99
  protected get logger(): import("../../services/logger.js").Logger;
100
100
  protected mirror?: WorkflowRunMirror;
101
+ protected readonly queueStrategy: 'per-workflow' | 'shared-groups';
102
+ protected readonly queueConcurrency: number;
103
+ protected readonly queueGroupConcurrency: number | GroupConcurrencyConfig;
101
104
  constructor(options?: {
102
105
  wireQueues?: boolean;
103
106
  mirror?: WorkflowRunMirror;
104
- });
107
+ } & WorkflowQueueOptions);
105
108
  private safeMirror;
106
109
  /**
107
110
  * Wire the queue-based orchestrator/step/sleeper workers.
@@ -519,4 +522,14 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
519
522
  */
520
523
  protected getOrchestratorQueueName(workflowName?: string): string;
521
524
  protected getStepWorkerQueueName(rpcName?: string): string;
525
+ /**
526
+ * Fairness key for a job on a shared queue. Under `'per-workflow'` the queue
527
+ * name already isolates workflows, so no group is needed — returning one
528
+ * anyway would cap a workflow inside its own dedicated queue.
529
+ *
530
+ * The tier repeats the id so a workflow can be given its own limit purely
531
+ * from config, with no per-workflow wiring; an unmatched tier falls back to
532
+ * the default limit.
533
+ */
534
+ protected getJobGroup(id?: string): JobGroup | undefined;
522
535
  }