@pikku/core 0.12.17 → 0.12.19

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 (58) hide show
  1. package/CHANGELOG.md +20 -13
  2. package/dist/function/function-runner.js +22 -1
  3. package/dist/function/functions.types.d.ts +1 -0
  4. package/dist/services/in-memory-session-store.d.ts +8 -0
  5. package/dist/services/in-memory-session-store.js +12 -0
  6. package/dist/services/in-memory-workflow-service.d.ts +5 -2
  7. package/dist/services/in-memory-workflow-service.js +3 -1
  8. package/dist/services/index.d.ts +2 -0
  9. package/dist/services/index.js +1 -0
  10. package/dist/services/session-store.d.ts +6 -0
  11. package/dist/services/session-store.js +1 -0
  12. package/dist/services/user-session-service.d.ts +12 -10
  13. package/dist/services/user-session-service.js +18 -21
  14. package/dist/services/workflow-service.d.ts +5 -2
  15. package/dist/testing/service-tests.d.ts +2 -0
  16. package/dist/testing/service-tests.js +50 -11
  17. package/dist/types/core.types.d.ts +3 -0
  18. package/dist/wirings/channel/channel-store.d.ts +5 -6
  19. package/dist/wirings/channel/local/local-channel-runner.js +7 -4
  20. package/dist/wirings/channel/serverless/serverless-channel-runner.js +25 -8
  21. package/dist/wirings/cli/channel/cli-channel-runner.js +4 -2
  22. package/dist/wirings/cli/cli-runner.js +5 -3
  23. package/dist/wirings/http/http-runner.js +1 -1
  24. package/dist/wirings/mcp/mcp-runner.js +1 -1
  25. package/dist/wirings/scheduler/scheduler-runner.js +1 -1
  26. package/dist/wirings/workflow/graph/graph-runner.js +4 -1
  27. package/dist/wirings/workflow/index.d.ts +1 -1
  28. package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
  29. package/dist/wirings/workflow/pikku-workflow-service.js +6 -2
  30. package/dist/wirings/workflow/workflow.types.d.ts +14 -0
  31. package/package.json +1 -1
  32. package/src/function/function-runner.ts +36 -1
  33. package/src/function/functions.types.ts +5 -14
  34. package/src/services/in-memory-session-store.test.ts +58 -0
  35. package/src/services/in-memory-session-store.ts +21 -0
  36. package/src/services/in-memory-workflow-service.test.ts +21 -0
  37. package/src/services/in-memory-workflow-service.ts +8 -1
  38. package/src/services/index.ts +2 -0
  39. package/src/services/session-store.ts +9 -0
  40. package/src/services/user-session-service.test.ts +53 -19
  41. package/src/services/user-session-service.ts +21 -22
  42. package/src/services/workflow-service.ts +6 -1
  43. package/src/testing/service-tests.ts +64 -11
  44. package/src/types/core.types.ts +3 -0
  45. package/src/wirings/channel/channel-store.ts +5 -8
  46. package/src/wirings/channel/local/local-channel-runner.ts +10 -4
  47. package/src/wirings/channel/local/local-eventhub-service.test.ts +0 -5
  48. package/src/wirings/channel/serverless/serverless-channel-runner.ts +30 -9
  49. package/src/wirings/cli/channel/cli-channel-runner.ts +4 -2
  50. package/src/wirings/cli/cli-runner.ts +7 -3
  51. package/src/wirings/http/http-runner.ts +3 -1
  52. package/src/wirings/mcp/mcp-runner.ts +3 -1
  53. package/src/wirings/scheduler/scheduler-runner.ts +1 -1
  54. package/src/wirings/workflow/graph/graph-runner.ts +5 -1
  55. package/src/wirings/workflow/index.ts +1 -0
  56. package/src/wirings/workflow/pikku-workflow-service.ts +13 -3
  57. package/src/wirings/workflow/workflow.types.ts +15 -0
  58. package/tsconfig.tsbuildinfo +1 -1
@@ -1,5 +1,5 @@
1
- import type { ChannelStore } from '../wirings/channel/channel-store.js'
2
1
  import type { CoreUserSession } from '../types/core.types.js'
2
+ import type { SessionStore } from './session-store.js'
3
3
 
4
4
  export interface SessionService<UserSession extends CoreUserSession> {
5
5
  sessionChanged: boolean
@@ -8,7 +8,7 @@ export interface SessionService<UserSession extends CoreUserSession> {
8
8
  freezeInitial(): UserSession | undefined
9
9
  set(session: UserSession): Promise<void> | void
10
10
  clear(): Promise<void> | void
11
- get(): Promise<UserSession> | UserSession | undefined
11
+ get(): UserSession | undefined
12
12
  }
13
13
 
14
14
  export class PikkuSessionService<UserSession extends CoreUserSession>
@@ -17,13 +17,16 @@ export class PikkuSessionService<UserSession extends CoreUserSession>
17
17
  public sessionChanged = false
18
18
  public initial: UserSession | undefined
19
19
  private session: UserSession | undefined
20
- constructor(
21
- private channelStore?: ChannelStore<unknown, unknown, UserSession>,
22
- private channelId?: string
23
- ) {
24
- if (channelStore && !channelId) {
25
- throw new Error('Channel ID is required when using channel store')
26
- }
20
+ private pikkuUserId?: string
21
+
22
+ constructor(private sessionStore?: SessionStore<UserSession>) {}
23
+
24
+ public setPikkuUserId(id: string) {
25
+ this.pikkuUserId = id
26
+ }
27
+
28
+ public getPikkuUserId(): string | undefined {
29
+ return this.pikkuUserId
27
30
  }
28
31
 
29
32
  public setInitial(session: UserSession) {
@@ -37,27 +40,23 @@ export class PikkuSessionService<UserSession extends CoreUserSession>
37
40
  return this.initial
38
41
  }
39
42
 
40
- public set(session: UserSession) {
43
+ public async set(session: UserSession) {
41
44
  this.sessionChanged = true
42
45
  this.session = session
43
- return this.channelStore?.setUserSession(this.channelId!, session)
46
+ if (this.sessionStore && this.pikkuUserId) {
47
+ await this.sessionStore.set(this.pikkuUserId, session)
48
+ }
44
49
  }
45
50
 
46
- public clear() {
51
+ public async clear() {
47
52
  this.sessionChanged = true
48
53
  this.session = undefined
49
- return this.channelStore?.setUserSession(this.channelId!, null)
54
+ if (this.sessionStore && this.pikkuUserId) {
55
+ await this.sessionStore.clear(this.pikkuUserId)
56
+ }
50
57
  }
51
58
 
52
- public get(): Promise<UserSession> | UserSession | undefined {
53
- if (this.channelStore) {
54
- const channel = this.channelStore.getChannelAndSession(this.channelId!)
55
- if (channel instanceof Promise) {
56
- return channel.then(({ session }) => session)
57
- } else {
58
- return channel.session
59
- }
60
- }
59
+ public get(): UserSession | undefined {
61
60
  return this.session
62
61
  }
63
62
  }
@@ -1,6 +1,7 @@
1
1
  import type { SerializedError } from '../types/core.types.js'
2
2
  import type {
3
3
  WorkflowRun,
4
+ WorkflowPlannedStep,
4
5
  WorkflowRunWire,
5
6
  WorkflowRunStatus,
6
7
  StepState,
@@ -19,7 +20,11 @@ export interface WorkflowService {
19
20
  input: any,
20
21
  inline: boolean,
21
22
  graphHash: string,
22
- wire: WorkflowRunWire
23
+ wire: WorkflowRunWire,
24
+ options?: {
25
+ deterministic?: boolean
26
+ plannedSteps?: WorkflowPlannedStep[]
27
+ }
23
28
  ): Promise<string>
24
29
  getRun(id: string): Promise<WorkflowRun | null>
25
30
  getRunStatus(id: string): Promise<WorkflowRunStatus | null>
@@ -11,6 +11,7 @@ import type { AIRunStateService } from '../services/ai-run-state-service.js'
11
11
  import type { SecretService } from '../services/secret-service.js'
12
12
  import type { CredentialService } from '../services/credential-service.js'
13
13
  import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
14
+ import type { SessionStore } from '../services/session-store.js'
14
15
 
15
16
  export interface ServiceTestConfig {
16
17
  name: string
@@ -34,6 +35,7 @@ export interface ServiceTestConfig {
34
35
  keyVersion?: number
35
36
  previousKey?: string
36
37
  }) => Promise<CredentialService & { rotateKEK?(): Promise<number> }>
38
+ sessionStore?: () => Promise<SessionStore>
37
39
  }
38
40
  }
39
41
 
@@ -49,32 +51,38 @@ export function defineServiceTests(config: ServiceTestConfig): void {
49
51
  store = await factory()
50
52
  })
51
53
 
52
- test('addChannel and getChannelAndSession', async () => {
54
+ test('addChannel and getChannel', async () => {
53
55
  await store.addChannel({
54
56
  channelId: 'ch-1',
55
57
  channelName: 'test-channel',
56
58
  openingData: { foo: 'bar' },
57
59
  })
58
60
 
59
- const result = await store.getChannelAndSession('ch-1')
61
+ const result = await store.getChannel('ch-1')
60
62
  assert.equal(result.channelId, 'ch-1')
61
63
  assert.equal(result.channelName, 'test-channel')
62
64
  assert.deepEqual(result.openingData, { foo: 'bar' })
63
- assert.deepEqual(result.session, {})
65
+ assert.equal(result.pikkuUserId, undefined)
64
66
  })
65
67
 
66
- test('setUserSession', async () => {
67
- const session = { userId: 'user-1' } as any
68
- await store.setUserSession('ch-1', session)
68
+ test('setPikkuUserId', async () => {
69
+ await store.setPikkuUserId('ch-1', 'user-1')
69
70
 
70
- const result = await store.getChannelAndSession('ch-1')
71
- assert.deepEqual(result.session, session)
71
+ const result = await store.getChannel('ch-1')
72
+ assert.equal(result.pikkuUserId, 'user-1')
72
73
  })
73
74
 
74
- test('getChannelAndSession throws for missing channel', async () => {
75
+ test('setPikkuUserId to null', async () => {
76
+ await store.setPikkuUserId('ch-1', null)
77
+
78
+ const result = await store.getChannel('ch-1')
79
+ assert.equal(result.pikkuUserId, undefined)
80
+ })
81
+
82
+ test('getChannel throws for missing channel', async () => {
75
83
  await assert.rejects(
76
84
  async () => {
77
- await store.getChannelAndSession('missing')
85
+ await store.getChannel('missing')
78
86
  },
79
87
  { message: 'Channel not found: missing' }
80
88
  )
@@ -87,7 +95,7 @@ export function defineServiceTests(config: ServiceTestConfig): void {
87
95
  })
88
96
  await store.removeChannels(['ch-2'])
89
97
  await assert.rejects(async () => {
90
- await store.getChannelAndSession('ch-2')
98
+ await store.getChannel('ch-2')
91
99
  })
92
100
  })
93
101
 
@@ -999,4 +1007,49 @@ export function defineServiceTests(config: ServiceTestConfig): void {
999
1007
  })
1000
1008
  })
1001
1009
  }
1010
+
1011
+ if (services.sessionStore) {
1012
+ const factory = services.sessionStore
1013
+ describe(`SessionStore [${name}]`, () => {
1014
+ let store: SessionStore
1015
+
1016
+ before(async () => {
1017
+ store = await factory()
1018
+ })
1019
+
1020
+ test('get returns undefined for unknown user', async () => {
1021
+ const result = await store.get('unknown-user')
1022
+ assert.equal(result, undefined)
1023
+ })
1024
+
1025
+ test('set and get round-trip', async () => {
1026
+ const session = { userId: 'user-1', organizationId: 'org-1' } as any
1027
+ await store.set('user-1', session)
1028
+
1029
+ const result = await store.get('user-1')
1030
+ assert.deepEqual(result, session)
1031
+ })
1032
+
1033
+ test('set overwrites previous session', async () => {
1034
+ await store.set('user-2', { userId: 'user-2', role: 'admin' } as any)
1035
+ await store.set('user-2', { userId: 'user-2', role: 'member' } as any)
1036
+
1037
+ const result = await store.get('user-2')
1038
+ assert.deepEqual(result, { userId: 'user-2', role: 'member' })
1039
+ })
1040
+
1041
+ test('clear removes session', async () => {
1042
+ await store.set('user-3', { userId: 'user-3' } as any)
1043
+ assert.ok(await store.get('user-3'))
1044
+
1045
+ await store.clear('user-3')
1046
+ const result = await store.get('user-3')
1047
+ assert.equal(result, undefined)
1048
+ })
1049
+
1050
+ test('clear is no-op for unknown user', async () => {
1051
+ await store.clear('nonexistent')
1052
+ })
1053
+ })
1054
+ }
1002
1055
  }
@@ -35,6 +35,7 @@ import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.
35
35
  import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
36
36
  import type { CredentialService } from '../services/credential-service.js'
37
37
  import type { MetaService } from '../services/meta-service.js'
38
+ import type { SessionStore } from '../services/session-store.js'
38
39
 
39
40
  export type PikkuWiringTypes =
40
41
  | 'http'
@@ -236,6 +237,8 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
236
237
  credentialService?: CredentialService
237
238
  /** Meta service for reading .pikku metadata files (filesystem on Node, R2/KV on CF) */
238
239
  metaService?: MetaService
240
+ /** Session store for persisting user sessions keyed by pikkuUserId */
241
+ sessionStore?: SessionStore
239
242
  }
240
243
 
241
244
  /**
@@ -1,5 +1,3 @@
1
- import type { CoreUserSession } from '../../types/core.types.js'
2
-
3
1
  export type Channel<ChannelType = unknown, OpeningData = unknown> = {
4
2
  channelId: string
5
3
  channelName: string
@@ -10,20 +8,19 @@ export type Channel<ChannelType = unknown, OpeningData = unknown> = {
10
8
  export abstract class ChannelStore<
11
9
  ChannelType = unknown,
12
10
  OpeningData = unknown,
13
- UserSession extends CoreUserSession = CoreUserSession,
14
11
  TypedChannel = Channel<ChannelType, OpeningData>,
15
12
  > {
16
13
  public abstract addChannel(
17
14
  channel: Channel<ChannelType, OpeningData>
18
15
  ): Promise<void> | void
19
16
  public abstract removeChannels(channelId: string[]): Promise<void> | void
20
- public abstract setUserSession(
17
+ public abstract setPikkuUserId(
21
18
  channelId: string,
22
- userSession: UserSession | null
19
+ pikkuUserId: string | null
23
20
  ): Promise<void> | void
24
- public abstract getChannelAndSession(
21
+ public abstract getChannel(
25
22
  channelId: string
26
23
  ):
27
- | Promise<TypedChannel & { session: UserSession }>
28
- | (TypedChannel & { session: UserSession })
24
+ | Promise<TypedChannel & { pikkuUserId?: string }>
25
+ | (TypedChannel & { pikkuUserId?: string })
29
26
  }
@@ -45,7 +45,7 @@ export const runLocalChannel = async ({
45
45
  let closedWireServices = false
46
46
 
47
47
  let channelHandler: PikkuLocalChannelHandler | undefined
48
- const userSession = new PikkuSessionService()
48
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
49
49
 
50
50
  let http: PikkuHTTP | undefined
51
51
  if (request) {
@@ -125,7 +125,9 @@ export const runLocalChannel = async ({
125
125
  singletonServices.logger.error(`Error handling onConnect: ${e}`)
126
126
  const errorResponse = getErrorResponse(e)
127
127
  channel.send({
128
- error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
128
+ error:
129
+ errorResponse?.message ??
130
+ (isProduction() ? 'Internal server error' : e.message),
129
131
  ...(!isProduction() && { errorName: e.constructor?.name }),
130
132
  })
131
133
  }
@@ -148,7 +150,9 @@ export const runLocalChannel = async ({
148
150
  singletonServices.logger.error(`Error handling onDisconnect: ${e}`)
149
151
  const errorResponse = getErrorResponse(e)
150
152
  channel.send({
151
- error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
153
+ error:
154
+ errorResponse?.message ??
155
+ (isProduction() ? 'Internal server error' : e.message),
152
156
  ...(!isProduction() && { errorName: e.constructor?.name }),
153
157
  })
154
158
  }
@@ -171,7 +175,9 @@ export const runLocalChannel = async ({
171
175
  singletonServices.logger.error(e)
172
176
  const errorResponse = getErrorResponse(e)
173
177
  channel.send({
174
- error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
178
+ error:
179
+ errorResponse?.message ??
180
+ (isProduction() ? 'Internal server error' : e.message),
175
181
  ...(!isProduction() && { errorName: e.constructor?.name }),
176
182
  })
177
183
  setTimeout(() => channel.close(), 200)
@@ -1,7 +1,6 @@
1
1
  import * as assert from 'assert'
2
2
  import { test } from 'node:test'
3
3
  import type { PikkuChannelHandler } from '../channel.types.js'
4
- import type { CoreUserSession } from '../../../types/core.types.js'
5
4
  import { LocalEventHubService } from './local-eventhub-service.js'
6
5
 
7
6
  class MockChannelHandler implements PikkuChannelHandler {
@@ -11,10 +10,6 @@ class MockChannelHandler implements PikkuChannelHandler {
11
10
  this.channelId = channelId
12
11
  }
13
12
 
14
- setUserSession(session: CoreUserSession): Promise<void> | void {
15
- throw new Error('Method not needed.')
16
- }
17
-
18
13
  getChannel() {
19
14
  return { channelId: this.channelId } as any
20
15
  }
@@ -89,7 +89,7 @@ export const runChannelConnect = async ({
89
89
  http = createHTTPWire(new PikkuFetchHTTPRequest(request), response)
90
90
  }
91
91
 
92
- const userSession = new PikkuSessionService(channelStore, channelId)
92
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
93
93
 
94
94
  const { channelConfig, openingData, meta } = await openChannel({
95
95
  channelId,
@@ -138,6 +138,13 @@ export const runChannelConnect = async ({
138
138
  channelMiddlewareMeta: meta.channelMiddleware,
139
139
  })
140
140
  }
141
+
142
+ // Store the pikkuUserId mapping after auth middleware has run
143
+ const pikkuUserId = userSession.getPikkuUserId()
144
+ if (pikkuUserId) {
145
+ await channelStore.setPikkuUserId(channelId, pikkuUserId)
146
+ }
147
+
141
148
  http?.response?.status(101)
142
149
  } catch (e: any) {
143
150
  handleHTTPError(
@@ -171,7 +178,7 @@ export const runChannelDisconnect = async ({
171
178
  // there's nothing to disconnect, so we can return early.
172
179
  let channelData
173
180
  try {
174
- channelData = await channelStore.getChannelAndSession(channelId)
181
+ channelData = await channelStore.getChannel(channelId)
175
182
  } catch {
176
183
  singletonServices.logger.info(
177
184
  `Channel ${channelId} not found during disconnect - already cleaned up`
@@ -179,7 +186,7 @@ export const runChannelDisconnect = async ({
179
186
  return
180
187
  }
181
188
 
182
- const { openingData, channelName, session } = channelData
189
+ const { openingData, channelName, pikkuUserId } = channelData
183
190
  const { channel, channelConfig, meta } = getVariablesForChannel({
184
191
  channelId,
185
192
  channelHandlerFactory,
@@ -187,8 +194,15 @@ export const runChannelDisconnect = async ({
187
194
  channelName,
188
195
  })
189
196
 
190
- const userSession = new PikkuSessionService(channelStore, channelId)
191
- userSession.setInitial(session)
197
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
198
+ if (pikkuUserId) {
199
+ userSession.setPikkuUserId(pikkuUserId)
200
+ const session = await singletonServices.sessionStore?.get(pikkuUserId)
201
+ if (session) {
202
+ userSession.setInitial(session)
203
+ }
204
+ }
205
+
192
206
  const wire: PikkuWire = {
193
207
  channel,
194
208
  ...createMiddlewareSessionWireProps(userSession),
@@ -237,8 +251,8 @@ export const runChannelMessage = async (
237
251
  const singletonServices = getSingletonServices()
238
252
  const createWireServices = getCreateWireServices()
239
253
  let wireServices: WireServices | undefined
240
- const { openingData, channelName, session } =
241
- await channelStore.getChannelAndSession(channelId)
254
+ const { openingData, channelName, pikkuUserId } =
255
+ await channelStore.getChannel(channelId)
242
256
 
243
257
  const { channel, channelHandler, channelConfig } = getVariablesForChannel({
244
258
  channelId,
@@ -247,8 +261,15 @@ export const runChannelMessage = async (
247
261
  channelName,
248
262
  })
249
263
 
250
- const userSession = new PikkuSessionService(channelStore, channelId)
251
- userSession.setInitial(session)
264
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
265
+ if (pikkuUserId) {
266
+ userSession.setPikkuUserId(pikkuUserId)
267
+ const session = await singletonServices.sessionStore?.get(pikkuUserId)
268
+ if (session) {
269
+ userSession.setInitial(session)
270
+ }
271
+ }
272
+
252
273
  const wire: PikkuWire = {
253
274
  channel,
254
275
  ...createMiddlewareSessionWireProps(userSession),
@@ -3,10 +3,12 @@ import type { CorePikkuCLIRender, CLIMeta } from '../cli.types.js'
3
3
  import { generateCommandHelp, parseCLIArguments } from '../command-parser.js'
4
4
 
5
5
  /**
6
- * Default JSON renderer for CLI output
6
+ * Default JSON renderer for CLI output. Emits single-line NDJSON so
7
+ * the output stays machine-parseable when the CLI is run with
8
+ * `--output json`; pretty-printing here would break NDJSON consumers.
7
9
  */
8
10
  const defaultJSONRenderer: CorePikkuCLIRender<any> = (_services, data) => {
9
- console.log(JSON.stringify(data, null, 2))
11
+ console.log(JSON.stringify(data))
10
12
  }
11
13
 
12
14
  /**
@@ -45,10 +45,12 @@ export class CLIError extends Error {
45
45
  }
46
46
 
47
47
  /**
48
- * Default JSON renderer for CLI output
48
+ * Default JSON renderer for CLI output. Emits single-line NDJSON so
49
+ * the output stays machine-parseable when the CLI is run with
50
+ * `--output json`; pretty-printing here would break NDJSON consumers.
49
51
  */
50
52
  const defaultJSONRenderer: CorePikkuCLIRender<any> = (_services, data) => {
51
- console.log(JSON.stringify(data, null, 2))
53
+ console.log(JSON.stringify(data))
52
54
  }
53
55
 
54
56
  /**
@@ -341,7 +343,9 @@ export async function runCLICommand({
341
343
  state: 'open',
342
344
  }
343
345
 
344
- const userSession = new PikkuSessionService<CoreUserSession>()
346
+ const userSession = new PikkuSessionService<CoreUserSession>(
347
+ singletonServices.sessionStore
348
+ )
345
349
 
346
350
  const wire: PikkuWire = {
347
351
  cli: {
@@ -303,10 +303,12 @@ const executeRoute = async (
303
303
  coerceDataFromSchema: boolean
304
304
  }
305
305
  ) => {
306
- const userSession = new PikkuSessionService<CoreUserSession>()
307
306
  const { params, route, meta } = matchedRoute
308
307
  const { singletonServices, createWireServices, skipUserSession, requestId } =
309
308
  services
309
+ const userSession = new PikkuSessionService<CoreUserSession>(
310
+ singletonServices.sessionStore
311
+ )
310
312
 
311
313
  // Attach URL parameters to the request object
312
314
  http?.request?.setParams(params)
@@ -214,7 +214,9 @@ async function runMCPPikkuFunc(
214
214
 
215
215
  singletonServices.logger.debug(`Running MCP ${type}: ${name}`)
216
216
 
217
- const mcpSessionService = new PikkuSessionService()
217
+ const mcpSessionService = new PikkuSessionService(
218
+ singletonServices.sessionStore
219
+ )
218
220
  const wire: PikkuWire = {
219
221
  mcp: mcpWire,
220
222
  ...createMiddlewareSessionWireProps(mcpSessionService),
@@ -82,7 +82,7 @@ export async function runScheduledTask({
82
82
  const task = pikkuState(null, 'scheduler', 'tasks').get(name)
83
83
  const meta = pikkuState(null, 'scheduler', 'meta')[name]
84
84
 
85
- const userSession = new PikkuSessionService()
85
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
86
86
  if (session) {
87
87
  userSession.set(session)
88
88
  }
@@ -852,7 +852,11 @@ export async function runWorkflowGraph(
852
852
  triggerInput,
853
853
  inline ?? false,
854
854
  meta.graphHash,
855
- wire ?? { type: 'unknown' }
855
+ wire ?? { type: 'unknown' },
856
+ {
857
+ deterministic: meta.deterministic,
858
+ plannedSteps: meta.plannedSteps,
859
+ }
856
860
  )
857
861
 
858
862
  if (inline) {
@@ -42,6 +42,7 @@ export type {
42
42
  export type {
43
43
  WorkflowService,
44
44
  WorkflowServiceConfig,
45
+ WorkflowPlannedStep,
45
46
  WorkflowRunWire,
46
47
  WorkflowStatus,
47
48
  WorkflowVersionStatus,
@@ -49,6 +49,7 @@ import type {
49
49
  PikkuWorkflowWire,
50
50
  StepState,
51
51
  StepStatus,
52
+ WorkflowPlannedStep,
52
53
  WorkflowRun,
53
54
  WorkflowRunStatus,
54
55
  WorkflowRunWire,
@@ -272,7 +273,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
272
273
  input: any,
273
274
  inline: boolean,
274
275
  graphHash: string,
275
- wire: WorkflowRunWire
276
+ wire: WorkflowRunWire,
277
+ options?: {
278
+ deterministic?: boolean
279
+ plannedSteps?: WorkflowPlannedStep[]
280
+ }
276
281
  ): Promise<string>
277
282
 
278
283
  /**
@@ -323,6 +328,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
323
328
  status: run.status,
324
329
  startedAt: run.createdAt,
325
330
  completedAt: terminalStatuses.has(run.status) ? run.updatedAt : undefined,
331
+ deterministic: run.deterministic,
332
+ plannedSteps: run.plannedSteps,
326
333
  steps,
327
334
  output: run.status === 'completed' ? run.output : undefined,
328
335
  error: run.error
@@ -689,7 +696,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
689
696
  input,
690
697
  shouldInline,
691
698
  workflowMeta.graphHash,
692
- wire
699
+ wire,
700
+ {
701
+ deterministic: workflowMeta.deterministic,
702
+ plannedSteps: workflowMeta.plannedSteps,
703
+ }
693
704
  )
694
705
 
695
706
  if (shouldInline) {
@@ -831,7 +842,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
831
842
  const wire: PikkuWire = {
832
843
  workflow: workflowWire,
833
844
  pikkuUserId: run.wire?.pikkuUserId,
834
- session: rpcService.wire?.session,
835
845
  rpc: rpcService.wire?.rpc,
836
846
  }
837
847
  try {
@@ -53,6 +53,11 @@ export interface WorkflowServiceConfig {
53
53
  sleeperRPCName: string
54
54
  }
55
55
 
56
+ export interface WorkflowPlannedStep {
57
+ /** Human-readable step label for UI timeline */
58
+ stepName: string
59
+ }
60
+
56
61
  /**
57
62
  * Workflow run status
58
63
  */
@@ -99,6 +104,10 @@ export interface WorkflowRun {
99
104
  inline?: boolean
100
105
  /** Graph hash of the workflow definition at run creation time */
101
106
  graphHash?: string
107
+ /** True when the workflow has a static, pre-computable step timeline */
108
+ deterministic?: boolean
109
+ /** Static planned steps snapshot captured at run start */
110
+ plannedSteps?: WorkflowPlannedStep[]
102
111
  /** Wire origin info (how this run was started) */
103
112
  wire: WorkflowRunWire
104
113
  /** Creation timestamp */
@@ -146,6 +155,8 @@ export interface WorkflowRunStatus {
146
155
  status: WorkflowStatus
147
156
  startedAt: Date
148
157
  completedAt?: Date
158
+ deterministic?: boolean
159
+ plannedSteps?: WorkflowPlannedStep[]
149
160
  steps: Array<{
150
161
  name: string
151
162
  status: StepStatus
@@ -270,6 +281,10 @@ export interface WorkflowRuntimeMeta {
270
281
  entryNodeIds?: string[]
271
282
  /** Hash of graph topology (nodes, edges, input mappings) */
272
283
  graphHash?: string
284
+ /** True when the workflow has a static, pre-computable step timeline */
285
+ deterministic?: boolean
286
+ /** Static planned steps metadata for deterministic workflows */
287
+ plannedSteps?: WorkflowPlannedStep[]
273
288
  }
274
289
 
275
290
  /**