@pikku/core 0.12.27 → 0.12.28

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 (30) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/function/function-runner.d.ts +1 -2
  3. package/dist/function/function-runner.js +2 -22
  4. package/dist/function/functions.types.d.ts +2 -0
  5. package/dist/services/meta-service.js +18 -7
  6. package/dist/types/core.types.d.ts +2 -0
  7. package/dist/wirings/channel/channel-common.js +9 -2
  8. package/dist/wirings/channel/local/local-channel-handler.d.ts +7 -4
  9. package/dist/wirings/channel/local/local-channel-handler.js +13 -5
  10. package/dist/wirings/channel/local/local-channel-runner.js +1 -1
  11. package/dist/wirings/workflow/pikku-workflow-service.js +10 -1
  12. package/package.json +1 -1
  13. package/src/dev/hot-reload.test.ts +2 -0
  14. package/src/function/function-runner.test.ts +2 -28
  15. package/src/function/function-runner.ts +2 -35
  16. package/src/function/functions.types.ts +2 -0
  17. package/src/services/meta-service.ts +17 -7
  18. package/src/types/core.types.ts +2 -0
  19. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +2 -0
  20. package/src/wirings/ai-agent/ai-agent-runner.test.ts +28 -4
  21. package/src/wirings/ai-agent/ai-agent-stream.test.ts +3 -0
  22. package/src/wirings/channel/channel-common.ts +9 -2
  23. package/src/wirings/channel/local/local-channel-handler.ts +24 -9
  24. package/src/wirings/channel/local/local-channel-runner.ts +2 -1
  25. package/src/wirings/cli/cli-runner.test.ts +4 -0
  26. package/src/wirings/http/http-runner.test.ts +1 -0
  27. package/src/wirings/queue/queue-runner.test.ts +1 -0
  28. package/src/wirings/scheduler/scheduler-runner.test.ts +10 -0
  29. package/src/wirings/workflow/pikku-workflow-service.ts +10 -1
  30. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.12.28
2
+
3
+ ### Patch Changes
4
+
5
+ - 2cf67be: Add inline option to pikkuFunc/pikkuSessionlessFunc for workflow step dispatch
6
+
7
+ By default, workflow steps now run inline (no queue hop). Set inline: false on a function to force dispatch through the queue for that step.
8
+
1
9
  ## 0.12.27
2
10
 
3
11
  ### Patch Changes
@@ -1,4 +1,4 @@
1
- import type { CoreServices, CoreUserSession, CorePikkuMiddleware, PikkuWiringTypes, PikkuWire, MiddlewareMetadata, PermissionMetadata, CoreSingletonServices, CreateWireServices } from '../types/core.types.js';
1
+ import type { CoreUserSession, CorePikkuMiddleware, PikkuWiringTypes, PikkuWire, MiddlewareMetadata, PermissionMetadata, CoreSingletonServices, CreateWireServices } from '../types/core.types.js';
2
2
  import type { CorePikkuChannelMiddleware } from '../wirings/channel/channel.types.js';
3
3
  import type { CorePermissionGroup, CorePikkuFunctionConfig, CorePikkuPermission } from './functions.types.js';
4
4
  import type { SessionService } from '../services/user-session-service.js';
@@ -6,7 +6,6 @@ import { PikkuCredentialWireService } from '../services/credential-wire-service.
6
6
  export declare const addFunction: (funcName: string, funcConfig: CorePikkuFunctionConfig<any, any>, packageName?: string | null) => void;
7
7
  export declare const getFunctionNames: (packageName?: string | null) => string[];
8
8
  export declare const getAllFunctionNames: () => string[];
9
- export declare const runPikkuFuncDirectly: <In, Out>(funcName: string, allServices: CoreServices, wire: PikkuWire, data: In, userSession?: SessionService<CoreUserSession>, packageName?: string | null) => Promise<Out>;
10
9
  export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTypes, wireId: string, funcName: string, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, inheritedPermissions, wirePermissions, coerceDataFromSchema, tags, wire, sessionService, credentialWireService, packageName, }: {
11
10
  singletonServices: CoreSingletonServices;
12
11
  createWireServices?: CreateWireServices;
@@ -4,7 +4,7 @@ import { runPermissions } from '../permissions.js';
4
4
  import { pikkuState } from '../pikku-state.js';
5
5
  import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js';
6
6
  import { parseVersionedId } from '../version.js';
7
- import { PikkuSessionService, createFunctionSessionWireProps, } from '../services/user-session-service.js';
7
+ import { PikkuSessionService } from '../services/user-session-service.js';
8
8
  import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js';
9
9
  import { PikkuCredentialWireService, createWireServicesCredentialWireProps, } from '../services/credential-wire-service.js';
10
10
  import { defaultPikkuUserIdResolver } from '../services/pikku-user-id.js';
@@ -132,17 +132,6 @@ export const getAllFunctionNames = () => {
132
132
  }
133
133
  return functions;
134
134
  };
135
- export const runPikkuFuncDirectly = async (funcName, allServices, wire, data, userSession, packageName = null) => {
136
- const funcConfig = pikkuState(packageName, 'function', 'functions').get(funcName);
137
- if (!funcConfig) {
138
- throw new Error(`Function not found: ${funcName}`);
139
- }
140
- const wireWithSession = {
141
- ...wire,
142
- ...(userSession && createFunctionSessionWireProps(userSession)),
143
- };
144
- return (await funcConfig.func(allServices, data, wireWithSession));
145
- };
146
135
  export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, inheritedPermissions, wirePermissions, coerceDataFromSchema, tags = [], wire, sessionService, credentialWireService, packageName = null, }) => {
147
136
  wire.wireType ??= wireType;
148
137
  wire.wireId ??= wireId;
@@ -230,7 +219,7 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
230
219
  }
231
220
  }
232
221
  }
233
- else if (funcMeta.sessionless === false) {
222
+ else {
234
223
  if (wiringAuth === false || funcConfig.auth === false) {
235
224
  resolvedSingletonServices.logger.warn(`Function '${funcName}' requires a session but auth was explicitly disabled — use pikkuSessionlessFunc instead.`);
236
225
  }
@@ -238,15 +227,6 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
238
227
  throw new ForbiddenError('Authentication required');
239
228
  }
240
229
  }
241
- else {
242
- // TODO: Remove after a couple of releases — backward compat for
243
- // generated metadata that doesn't include the `sessionless` field yet.
244
- if (wiringAuth === true || funcConfig.auth === true) {
245
- if (!session) {
246
- throw new ForbiddenError('Authentication required');
247
- }
248
- }
249
- }
250
230
  if (session?.readonly && !funcMeta.readonly) {
251
231
  throw new ReadonlySessionError();
252
232
  }
@@ -149,6 +149,8 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
149
149
  readonly?: boolean;
150
150
  deploy?: 'serverless' | 'server' | 'auto';
151
151
  approvalRequired?: boolean;
152
+ /** When false, workflow steps calling this function are dispatched via the queue instead of running inline. Defaults to true (inline). */
153
+ inline?: boolean;
152
154
  audit?: boolean | {
153
155
  durability?: 'best-effort' | 'transactional';
154
156
  };
@@ -285,15 +285,26 @@ export class LocalMetaService {
285
285
  if (!emailsMeta.src) {
286
286
  throw new Error('No generated email metadata found. Run `pikku emails generate`.');
287
287
  }
288
+ // emailsMeta.src is an absolute path resolved by the CLI at generation time.
289
+ // Read directly rather than through readProjectFile (which prepends the project
290
+ // root and produces a wrong compound path when baseDir is absolute).
288
291
  const baseDir = emailsMeta.src;
292
+ const readEmailFile = async (rel) => {
293
+ try {
294
+ return await readFile(join(baseDir, rel), 'utf-8');
295
+ }
296
+ catch {
297
+ return null;
298
+ }
299
+ };
289
300
  const [themeRaw, localeRaw, layoutRaw, footerRaw, templateHtml, templateSubject, templateText,] = await Promise.all([
290
- this.readProjectFile(`${baseDir}/theme.json`),
291
- this.readProjectFile(`${baseDir}/locales/${locale}.json`),
292
- this.readProjectFile(`${baseDir}/partials/layout.html`),
293
- this.readProjectFile(`${baseDir}/partials/footer.html`),
294
- this.readProjectFile(`${baseDir}/templates/${templateName}.html`),
295
- this.readProjectFile(`${baseDir}/templates/${templateName}.subject.txt`),
296
- this.readProjectFile(`${baseDir}/templates/${templateName}.text.txt`),
301
+ readEmailFile('theme.json'),
302
+ readEmailFile(`locales/${locale}.json`),
303
+ readEmailFile('partials/layout.html'),
304
+ readEmailFile('partials/footer.html'),
305
+ readEmailFile(`templates/${templateName}.html`),
306
+ readEmailFile(`templates/${templateName}.subject.txt`),
307
+ readEmailFile(`templates/${templateName}.text.txt`),
297
308
  ]);
298
309
  return {
299
310
  theme: themeRaw ? JSON.parse(themeRaw) : {},
@@ -82,6 +82,8 @@ export type FunctionRuntimeMeta = {
82
82
  readonly?: boolean;
83
83
  deploy?: 'serverless' | 'server' | 'auto';
84
84
  sessionless?: boolean;
85
+ /** When false, workflow steps calling this function are dispatched via the queue. Defaults to true (inline). */
86
+ inline?: boolean;
85
87
  version?: number;
86
88
  approvalRequired?: boolean;
87
89
  approvalDescription?: string;
@@ -1,5 +1,5 @@
1
1
  import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
2
- import { runPikkuFuncDirectly } from '../../function/function-runner.js';
2
+ import { runPikkuFunc } from '../../function/function-runner.js';
3
3
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from './channel-middleware-runner.js';
4
4
  /**
5
5
  * Runs a channel lifecycle function (onConnect or onDisconnect) with proper middleware handling.
@@ -37,7 +37,14 @@ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, l
37
37
  wire = wrapChannelWithMiddleware(wire, services, allChannelMiddleware);
38
38
  }
39
39
  const runLifecycle = async () => {
40
- return await runPikkuFuncDirectly(meta.pikkuFuncId, services, wire, data);
40
+ return await runPikkuFunc('channel', channelConfig.name, meta.pikkuFuncId, {
41
+ singletonServices: services,
42
+ data: () => data,
43
+ wire,
44
+ tags: meta.tags ?? [],
45
+ inheritedPermissions: meta.permissions,
46
+ packageName: meta.packageName ?? null,
47
+ });
41
48
  };
42
49
  if (allMiddleware.length > 0) {
43
50
  return await runMiddleware(services, wire, allMiddleware, runLifecycle);
@@ -1,3 +1,4 @@
1
+ import type { Logger } from '../../../services/logger.js';
1
2
  import type { BinaryData } from '../channel.types.js';
2
3
  import { PikkuAbstractChannelHandler } from '../pikku-abstract-channel-handler.js';
3
4
  export declare class PikkuLocalChannelHandler<OpeningData = unknown, Out = unknown> extends PikkuAbstractChannelHandler<OpeningData, Out> {
@@ -7,14 +8,16 @@ export declare class PikkuLocalChannelHandler<OpeningData = unknown, Out = unkno
7
8
  private closeCallbacks;
8
9
  private sendCallback?;
9
10
  private sendBinaryCallback?;
10
- registerOnOpen(callback: () => void): void;
11
- open(): void;
11
+ private logger?;
12
+ constructor(channelId: string, channelName: string, openingData: OpeningData, logger?: Logger);
13
+ registerOnOpen(callback: () => Promise<void> | void): void;
14
+ open(): Promise<void>;
12
15
  registerOnMessage(callback: (data: any) => Promise<unknown>): void;
13
16
  message(data: unknown): Promise<unknown>;
14
17
  registerOnBinaryMessage(callback: (data: BinaryData) => Promise<BinaryData | void> | BinaryData | void): void;
15
18
  binaryMessage(data: BinaryData): Promise<BinaryData | void>;
16
- registerOnClose(callback: () => void): void;
17
- close(): void;
19
+ registerOnClose(callback: () => Promise<void> | void): void;
20
+ close(): Promise<void>;
18
21
  registerOnSend(send: (message: Out) => void): void;
19
22
  send(message: Out, isBinary?: boolean): void;
20
23
  registerOnSendBinary(send: (data: BinaryData) => void): void;
@@ -6,13 +6,18 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
6
6
  closeCallbacks = [];
7
7
  sendCallback;
8
8
  sendBinaryCallback;
9
+ logger;
10
+ constructor(channelId, channelName, openingData, logger) {
11
+ super(channelId, channelName, openingData);
12
+ this.logger = logger;
13
+ }
9
14
  registerOnOpen(callback) {
10
15
  this.openCallBack = callback;
11
16
  }
12
- open() {
17
+ async open() {
13
18
  this.getChannel().state = 'open';
14
19
  if (this.openCallBack) {
15
- this.openCallBack();
20
+ await this.openCallBack();
16
21
  }
17
22
  }
18
23
  registerOnMessage(callback) {
@@ -30,13 +35,16 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
30
35
  registerOnClose(callback) {
31
36
  this.closeCallbacks.push(callback);
32
37
  }
33
- close() {
38
+ async close() {
34
39
  if (this.getChannel().state === 'closed') {
35
40
  return;
36
41
  }
37
42
  super.close();
38
- for (const cb of this.closeCallbacks) {
39
- cb();
43
+ const results = await Promise.allSettled(this.closeCallbacks.map((cb) => cb()));
44
+ for (const result of results) {
45
+ if (result.status === 'rejected') {
46
+ this.logger?.error('Error in channel close callback:', result.reason);
47
+ }
40
48
  }
41
49
  }
42
50
  registerOnSend(send) {
@@ -59,7 +59,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
59
59
  }
60
60
  const main = async () => {
61
61
  try {
62
- channelHandler = new PikkuLocalChannelHandler(channelId, channelConfig.name, openingData);
62
+ channelHandler = new PikkuLocalChannelHandler(channelId, channelConfig.name, openingData, singletonServices.logger);
63
63
  const channel = channelHandler.getChannel();
64
64
  const wire = {
65
65
  channel,
@@ -484,7 +484,16 @@ export class PikkuWorkflowService {
484
484
  * through to the inline execution path.
485
485
  */
486
486
  async dispatchStep(runId, stepName, rpcName, data, stepOptions) {
487
- if (this.isInline(runId) || !getSingletonServices()?.queueService) {
487
+ if (!getSingletonServices()?.queueService) {
488
+ return false;
489
+ }
490
+ // Functions default to inline execution. Only dispatch via queue when the
491
+ // function explicitly sets inline: false.
492
+ const functionsMeta = pikkuState(null, 'function', 'meta');
493
+ const rpcFuncId = pikkuState(null, 'rpc', 'meta')[rpcName];
494
+ const rpcMeta = typeof rpcFuncId === 'string' ? functionsMeta[rpcFuncId] : undefined;
495
+ const forceQueue = rpcMeta?.inline === false;
496
+ if (!forceQueue && this.isInline(runId)) {
488
497
  return false;
489
498
  }
490
499
  const retries = stepOptions?.retries ?? 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.27",
3
+ "version": "0.12.28",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -308,6 +308,7 @@ describe('pikkuDevReloader', { concurrency: false }, () => {
308
308
  pikkuFuncId: 'hotTask',
309
309
  inputSchemaName: null,
310
310
  outputSchemaName: null,
311
+ sessionless: true,
311
312
  }
312
313
 
313
314
  addFunction('hotTask', {
@@ -375,6 +376,7 @@ describe('pikkuDevReloader', { concurrency: false }, () => {
375
376
  pikkuFuncId: 'queue_hot-queue',
376
377
  inputSchemaName: null,
377
378
  outputSchemaName: null,
379
+ sessionless: true,
378
380
  }
379
381
 
380
382
  addFunction('queue_hot-queue', {
@@ -5,7 +5,6 @@ import {
5
5
  getAllFunctionNames,
6
6
  getFunctionNames,
7
7
  runPikkuFunc,
8
- runPikkuFuncDirectly,
9
8
  } from './function-runner.js'
10
9
  import { addTagMiddleware, addTagPermission } from '../index.js'
11
10
  import { resetPikkuState, pikkuState } from '../pikku-state.js'
@@ -33,6 +32,7 @@ const addTestFunction = (funcName: string, funcConfig: any) => {
33
32
  pikkuFuncId: funcName,
34
33
  inputSchemaName: null,
35
34
  outputSchemaName: null,
35
+ sessionless: true,
36
36
  middleware,
37
37
  permissions,
38
38
  }
@@ -676,7 +676,7 @@ describe('runPikkuFunc - Integration Tests', () => {
676
676
  )
677
677
  })
678
678
 
679
- test('should use backward-compatible auth checks when sessionless metadata is absent', async () => {
679
+ test('should require auth when sessionless metadata is absent (undefined defaults to session-required)', async () => {
680
680
  addTestFunction('legacyAuth', {
681
681
  func: async () => 'ok',
682
682
  auth: true,
@@ -1227,32 +1227,6 @@ describe('runPikkuFunc - Integration Tests', () => {
1227
1227
  })
1228
1228
 
1229
1229
  describe('function-runner helpers', () => {
1230
- test('runPikkuFuncDirectly should pass through the provided wire and session helpers', async () => {
1231
- let receivedWire: any
1232
- const sessionService = new PikkuSessionService({
1233
- get: async () => undefined,
1234
- } as any)
1235
-
1236
- addTestFunction('directFunc', {
1237
- func: async (_services: any, _data: any, wire: any) => {
1238
- receivedWire = wire
1239
- return 'ok'
1240
- },
1241
- })
1242
-
1243
- const result = await runPikkuFuncDirectly(
1244
- 'directFunc',
1245
- mockServices,
1246
- { traceId: 'trace-1' },
1247
- { hello: 'world' },
1248
- sessionService
1249
- )
1250
-
1251
- assert.equal(result, 'ok')
1252
- assert.equal(receivedWire.traceId, 'trace-1')
1253
- assert.equal(typeof receivedWire.getSession, 'function')
1254
- })
1255
-
1256
1230
  test('getFunctionNames and getAllFunctionNames should include addon namespaces', () => {
1257
1231
  addTestFunction('rootFunc', { func: async () => 'ok' })
1258
1232
  pikkuState(null, 'addons', 'packages').set('stripe', {
@@ -7,7 +7,6 @@ import { runPermissions } from '../permissions.js'
7
7
  import { pikkuState } from '../pikku-state.js'
8
8
  import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js'
9
9
  import type {
10
- CoreServices,
11
10
  CoreUserSession,
12
11
  CorePikkuMiddleware,
13
12
  PikkuWiringTypes,
@@ -26,10 +25,7 @@ import type {
26
25
  } from './functions.types.js'
27
26
  import { parseVersionedId } from '../version.js'
28
27
  import type { SessionService } from '../services/user-session-service.js'
29
- import {
30
- PikkuSessionService,
31
- createFunctionSessionWireProps,
32
- } from '../services/user-session-service.js'
28
+ import { PikkuSessionService } from '../services/user-session-service.js'
33
29
  import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js'
34
30
  import {
35
31
  PikkuCredentialWireService,
@@ -200,27 +196,6 @@ export const getAllFunctionNames = (): string[] => {
200
196
  return functions
201
197
  }
202
198
 
203
- export const runPikkuFuncDirectly = async <In, Out>(
204
- funcName: string,
205
- allServices: CoreServices,
206
- wire: PikkuWire,
207
- data: In,
208
- userSession?: SessionService<CoreUserSession>,
209
- packageName: string | null = null
210
- ) => {
211
- const funcConfig = pikkuState(packageName, 'function', 'functions').get(
212
- funcName
213
- )
214
- if (!funcConfig) {
215
- throw new Error(`Function not found: ${funcName}`)
216
- }
217
- const wireWithSession = {
218
- ...wire,
219
- ...(userSession && createFunctionSessionWireProps(userSession)),
220
- }
221
- return (await funcConfig.func(allServices, data, wireWithSession)) as Out
222
- }
223
-
224
199
  export const runPikkuFunc = async <In = any, Out = any>(
225
200
  wireType: PikkuWiringTypes,
226
201
  wireId: string,
@@ -381,7 +356,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
381
356
  throw new ForbiddenError('Authentication required')
382
357
  }
383
358
  }
384
- } else if (funcMeta.sessionless === false) {
359
+ } else {
385
360
  if (wiringAuth === false || funcConfig.auth === false) {
386
361
  resolvedSingletonServices.logger.warn(
387
362
  `Function '${funcName}' requires a session but auth was explicitly disabled — use pikkuSessionlessFunc instead.`
@@ -390,14 +365,6 @@ export const runPikkuFunc = async <In = any, Out = any>(
390
365
  if (!session) {
391
366
  throw new ForbiddenError('Authentication required')
392
367
  }
393
- } else {
394
- // TODO: Remove after a couple of releases — backward compat for
395
- // generated metadata that doesn't include the `sessionless` field yet.
396
- if (wiringAuth === true || funcConfig.auth === true) {
397
- if (!session) {
398
- throw new ForbiddenError('Authentication required')
399
- }
400
- }
401
368
  }
402
369
 
403
370
  if ((session as any)?.readonly && !funcMeta.readonly) {
@@ -295,6 +295,8 @@ export type CorePikkuFunctionConfig<
295
295
  readonly?: boolean
296
296
  deploy?: 'serverless' | 'server' | 'auto'
297
297
  approvalRequired?: boolean
298
+ /** When false, workflow steps calling this function are dispatched via the queue instead of running inline. Defaults to true (inline). */
299
+ inline?: boolean
298
300
  audit?:
299
301
  | boolean
300
302
  | {
@@ -533,7 +533,17 @@ export class LocalMetaService implements MetaService {
533
533
  )
534
534
  }
535
535
 
536
+ // emailsMeta.src is an absolute path resolved by the CLI at generation time.
537
+ // Read directly rather than through readProjectFile (which prepends the project
538
+ // root and produces a wrong compound path when baseDir is absolute).
536
539
  const baseDir = emailsMeta.src
540
+ const readEmailFile = async (rel: string): Promise<string | null> => {
541
+ try {
542
+ return await readFile(join(baseDir, rel), 'utf-8')
543
+ } catch {
544
+ return null
545
+ }
546
+ }
537
547
  const [
538
548
  themeRaw,
539
549
  localeRaw,
@@ -543,13 +553,13 @@ export class LocalMetaService implements MetaService {
543
553
  templateSubject,
544
554
  templateText,
545
555
  ] = await Promise.all([
546
- this.readProjectFile(`${baseDir}/theme.json`),
547
- this.readProjectFile(`${baseDir}/locales/${locale}.json`),
548
- this.readProjectFile(`${baseDir}/partials/layout.html`),
549
- this.readProjectFile(`${baseDir}/partials/footer.html`),
550
- this.readProjectFile(`${baseDir}/templates/${templateName}.html`),
551
- this.readProjectFile(`${baseDir}/templates/${templateName}.subject.txt`),
552
- this.readProjectFile(`${baseDir}/templates/${templateName}.text.txt`),
556
+ readEmailFile('theme.json'),
557
+ readEmailFile(`locales/${locale}.json`),
558
+ readEmailFile('partials/layout.html'),
559
+ readEmailFile('partials/footer.html'),
560
+ readEmailFile(`templates/${templateName}.html`),
561
+ readEmailFile(`templates/${templateName}.subject.txt`),
562
+ readEmailFile(`templates/${templateName}.text.txt`),
553
563
  ])
554
564
 
555
565
  return {
@@ -118,6 +118,8 @@ export type FunctionRuntimeMeta = {
118
118
  readonly?: boolean
119
119
  deploy?: 'serverless' | 'server' | 'auto'
120
120
  sessionless?: boolean
121
+ /** When false, workflow steps calling this function are dispatched via the queue. Defaults to true (inline). */
122
+ inline?: boolean
121
123
  version?: number
122
124
  approvalRequired?: boolean
123
125
  approvalDescription?: string
@@ -178,6 +178,7 @@ describe('ai-agent-prepare', () => {
178
178
  description: 'Deploy the service',
179
179
  approvalRequired: true,
180
180
  inputSchemaName: 'DeployInput',
181
+ sessionless: true,
181
182
  }
182
183
  pikkuState(null, 'misc', 'schemas').set('DeployInput', {
183
184
  type: 'object',
@@ -254,6 +255,7 @@ describe('ai-agent-prepare', () => {
254
255
  description: 'Secret tool',
255
256
  permissions: ['admin'],
256
257
  inputSchemaName: 'SecretInput',
258
+ sessionless: true,
257
259
  }
258
260
  pikkuState(null, 'misc', 'schemas').set('SecretInput', {
259
261
  type: 'object',
@@ -560,6 +560,7 @@ describe('runAIAgent', () => {
560
560
  description: 'Deploy',
561
561
  approvalRequired: true,
562
562
  inputSchemaName: 'DeployInput',
563
+ sessionless: true,
563
564
  }
564
565
  pikkuState(null, 'misc', 'schemas').set('DeployInput', {
565
566
  type: 'object',
@@ -716,6 +717,7 @@ describe('resumeAIAgentSync', () => {
716
717
  pikkuState(null, 'function', 'meta').deploy = {
717
718
  description: 'Deploy',
718
719
  inputSchemaName: 'DeployInput',
720
+ sessionless: true,
719
721
  }
720
722
  pikkuState(null, 'misc', 'schemas').set('DeployInput', {
721
723
  type: 'object',
@@ -901,6 +903,7 @@ describe('resumeAIAgentSync', () => {
901
903
  pikkuState(null, 'function', 'meta').deploy = {
902
904
  description: 'Deploy',
903
905
  inputSchemaName: 'DeployInput',
906
+ sessionless: true,
904
907
  }
905
908
  pikkuState(null, 'misc', 'schemas').set('DeployInput', {
906
909
  type: 'object',
@@ -986,7 +989,12 @@ describe('getCredential API key override', () => {
986
989
  }
987
990
 
988
991
  const mockServices = {
989
- logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
992
+ logger: {
993
+ info: () => {},
994
+ warn: () => {},
995
+ error: () => {},
996
+ debug: () => {},
997
+ },
990
998
  aiAgentRunner: {
991
999
  run: async (): Promise<AIAgentStepResult> => {
992
1000
  originalRunCalls.push('original')
@@ -1019,13 +1027,24 @@ describe('getCredential API key override', () => {
1019
1027
  const originalRunCalls: string[] = []
1020
1028
 
1021
1029
  const mockServices = {
1022
- logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
1030
+ logger: {
1031
+ info: () => {},
1032
+ warn: () => {},
1033
+ error: () => {},
1034
+ debug: () => {},
1035
+ },
1023
1036
  aiAgentRunner: {
1024
1037
  run: async (): Promise<AIAgentStepResult> => {
1025
1038
  originalRunCalls.push('original')
1026
1039
  return makeStepResult({ text: 'from-original', finishReason: 'stop' })
1027
1040
  },
1028
- withApiKey: (_key: string) => ({ run: async () => makeStepResult({ text: 'should-not-be-called', finishReason: 'stop' }) }),
1041
+ withApiKey: (_key: string) => ({
1042
+ run: async () =>
1043
+ makeStepResult({
1044
+ text: 'should-not-be-called',
1045
+ finishReason: 'stop',
1046
+ }),
1047
+ }),
1029
1048
  },
1030
1049
  aiRunState: {
1031
1050
  createRun: async () => 'run-no-cred',
@@ -1051,7 +1070,12 @@ describe('getCredential API key override', () => {
1051
1070
  const originalRunCalls: string[] = []
1052
1071
 
1053
1072
  const mockServices = {
1054
- logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
1073
+ logger: {
1074
+ info: () => {},
1075
+ warn: () => {},
1076
+ error: () => {},
1077
+ debug: () => {},
1078
+ },
1055
1079
  aiAgentRunner: {
1056
1080
  run: async (): Promise<AIAgentStepResult> => {
1057
1081
  originalRunCalls.push('original')
@@ -327,6 +327,7 @@ describe('streamAIAgent', () => {
327
327
  description: 'Add a todo',
328
328
  approvalRequired: true,
329
329
  inputSchemaName: 'AddTodoInput',
330
+ sessionless: true,
330
331
  }
331
332
  pikkuState(null, 'misc', 'schemas').set('AddTodoInput', {
332
333
  type: 'object',
@@ -428,6 +429,7 @@ describe('streamAIAgent', () => {
428
429
  description: 'Add a todo',
429
430
  approvalRequired: true,
430
431
  inputSchemaName: 'AddTodoInput',
432
+ sessionless: true,
431
433
  }
432
434
  pikkuState('@test/addon-todos', 'misc', 'schemas').set('AddTodoInput', {
433
435
  type: 'object',
@@ -1287,6 +1289,7 @@ describe('resumeAIAgent', () => {
1287
1289
  pikkuState(null, 'function', 'meta').deploy = {
1288
1290
  description: 'Deploy',
1289
1291
  inputSchemaName: 'DeployInput',
1292
+ sessionless: true,
1290
1293
  }
1291
1294
  pikkuState(null, 'misc', 'schemas').set('DeployInput', {
1292
1295
  type: 'object',
@@ -6,7 +6,7 @@ import type {
6
6
  } from '../../types/core.types.js'
7
7
  import type { CoreChannel, ChannelMessageMeta } from './channel.types.js'
8
8
  import { combineMiddleware, runMiddleware } from '../../middleware-runner.js'
9
- import { runPikkuFuncDirectly } from '../../function/function-runner.js'
9
+ import { runPikkuFunc } from '../../function/function-runner.js'
10
10
  import {
11
11
  combineChannelMiddleware,
12
12
  wrapChannelWithMiddleware,
@@ -79,7 +79,14 @@ export const runChannelLifecycleWithMiddleware = async ({
79
79
  }
80
80
 
81
81
  const runLifecycle = async () => {
82
- return await runPikkuFuncDirectly(meta.pikkuFuncId, services, wire, data)
82
+ return await runPikkuFunc('channel', channelConfig.name, meta.pikkuFuncId, {
83
+ singletonServices: services,
84
+ data: () => data as any,
85
+ wire,
86
+ tags: meta.tags ?? [],
87
+ inheritedPermissions: meta.permissions,
88
+ packageName: meta.packageName ?? null,
89
+ })
83
90
  }
84
91
 
85
92
  if (allMiddleware.length > 0) {