@pikku/core 0.12.3 → 0.12.5

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.
@@ -133,6 +133,14 @@ const combinedPermissionsCache: Record<
133
133
  gateway: {},
134
134
  }
135
135
 
136
+ export const clearPermissionsCache = () => {
137
+ for (const key of Object.keys(
138
+ combinedPermissionsCache
139
+ ) as PikkuWiringTypes[]) {
140
+ combinedPermissionsCache[key] = {}
141
+ }
142
+ }
143
+
136
144
  /**
137
145
  * Combines wiring-specific permissions with function-level permissions.
138
146
  *
@@ -142,12 +142,12 @@ describe('InMemoryWorkflowService', () => {
142
142
  assert.ok(updated.failedAt)
143
143
  })
144
144
 
145
- test('should return default step state for non-existent step', async () => {
145
+ test('should throw for non-existent step', async () => {
146
146
  const runId = await service.createRun('wf', {}, true, 'h', {} as any)
147
- const step = await service.getStepState(runId, 'non-existent')
148
- assert.strictEqual(step.stepId, '')
149
- assert.strictEqual(step.status, 'pending')
150
- assert.strictEqual(step.attemptCount, 0)
147
+ await assert.rejects(
148
+ () => service.getStepState(runId, 'non-existent'),
149
+ /Step not found/
150
+ )
151
151
  })
152
152
  })
153
153
 
@@ -3,6 +3,7 @@ import { PikkuWorkflowService } from '../wirings/workflow/pikku-workflow-service
3
3
  import type { SerializedError } from '../types/core.types.js'
4
4
  import type {
5
5
  WorkflowRun,
6
+ WorkflowRunService,
6
7
  WorkflowRunWire,
7
8
  StepState,
8
9
  WorkflowStatus,
@@ -31,7 +32,10 @@ interface InternalStepData {
31
32
  * await workflowService.startWorkflow('myWorkflow', input, { type: 'cli' }, rpc, { inline: true })
32
33
  * ```
33
34
  */
34
- export class InMemoryWorkflowService extends PikkuWorkflowService {
35
+ export class InMemoryWorkflowService
36
+ extends PikkuWorkflowService
37
+ implements WorkflowRunService
38
+ {
35
39
  private runs = new Map<string, WorkflowRun>()
36
40
  private steps = new Map<string, StepState>() // keyed by `${runId}:${stepName}`
37
41
  private stepData = new Map<string, InternalStepData>() // keyed by stepId
@@ -114,7 +118,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
114
118
  const stepId = randomUUID()
115
119
  const now = new Date()
116
120
 
117
- const step: StepState = {
121
+ const step: StepState & { stepName: string } = {
118
122
  stepId,
119
123
  status: 'pending',
120
124
  attemptCount: 1,
@@ -122,15 +126,16 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
122
126
  retryDelay: stepOptions?.retryDelay,
123
127
  createdAt: now,
124
128
  updatedAt: now,
129
+ stepName,
125
130
  }
126
131
 
127
132
  const key = `${runId}:${stepName}`
128
133
  this.steps.set(key, step)
129
134
  this.stepData.set(stepId, { rpcName, data, stepName })
130
135
 
131
- // Add to history
136
+ // Add to history (same reference so mutations are reflected)
132
137
  const history = this.stepHistory.get(runId) || []
133
- history.push({ ...step, stepName })
138
+ history.push(step)
134
139
  this.stepHistory.set(runId, history)
135
140
 
136
141
  return step
@@ -140,13 +145,9 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
140
145
  const key = `${runId}:${stepName}`
141
146
  const step = this.steps.get(key)
142
147
  if (!step) {
143
- return {
144
- stepId: '',
145
- status: 'pending',
146
- attemptCount: 0,
147
- createdAt: new Date(),
148
- updatedAt: new Date(),
149
- }
148
+ throw new Error(
149
+ `Step not found: runId=${runId}, stepName=${stepName}. Use insertStepState to create it.`
150
+ )
150
151
  }
151
152
  return step
152
153
  }
@@ -228,7 +229,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
228
229
  const newStepId = randomUUID()
229
230
  const now = new Date()
230
231
 
231
- const newStep: StepState = {
232
+ const newStep: StepState & { stepName: string } = {
232
233
  stepId: newStepId,
233
234
  status,
234
235
  attemptCount: failedStep.attemptCount + 1,
@@ -236,6 +237,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
236
237
  retryDelay: failedStep.retryDelay,
237
238
  createdAt: now,
238
239
  updatedAt: now,
240
+ stepName: stepName,
239
241
  }
240
242
 
241
243
  if (status === 'running') {
@@ -250,14 +252,76 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
250
252
  this.stepData.set(newStepId, { ...failedStepData })
251
253
  }
252
254
 
253
- // Add to history
255
+ // Add to history (same reference so mutations are reflected)
254
256
  const history = this.stepHistory.get(runId) || []
255
- history.push({ ...newStep, stepName })
257
+ history.push(newStep)
256
258
  this.stepHistory.set(runId, history)
257
259
 
258
260
  return newStep
259
261
  }
260
262
 
263
+ async listRuns(options?: {
264
+ workflowName?: string
265
+ status?: string
266
+ limit?: number
267
+ offset?: number
268
+ }): Promise<WorkflowRun[]> {
269
+ let runs = Array.from(this.runs.values())
270
+ if (options?.workflowName) {
271
+ runs = runs.filter((r) => r.workflow === options.workflowName)
272
+ }
273
+ if (options?.status) {
274
+ runs = runs.filter((r) => r.status === options.status)
275
+ }
276
+ runs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
277
+ const offset = options?.offset ?? 0
278
+ const limit = options?.limit ?? runs.length
279
+ return runs.slice(offset, offset + limit)
280
+ }
281
+
282
+ async getRunSteps(
283
+ runId: string
284
+ ): Promise<
285
+ Array<StepState & { stepName: string; rpcName?: string; data?: any }>
286
+ > {
287
+ const history = this.stepHistory.get(runId) || []
288
+ return history.map((step) => {
289
+ const stepDataEntry = this.stepData.get(step.stepId)
290
+ return {
291
+ ...step,
292
+ rpcName: stepDataEntry?.rpcName ?? undefined,
293
+ data: stepDataEntry?.data,
294
+ }
295
+ })
296
+ }
297
+
298
+ async getDistinctWorkflowNames(): Promise<string[]> {
299
+ const names = new Set<string>()
300
+ for (const run of this.runs.values()) {
301
+ names.add(run.workflow)
302
+ }
303
+ return Array.from(names)
304
+ }
305
+
306
+ async deleteRun(id: string): Promise<boolean> {
307
+ const existed = this.runs.has(id)
308
+ this.runs.delete(id)
309
+ this.stepHistory.delete(id)
310
+ this.runState.delete(id)
311
+ const prefix = `${id}:`
312
+ for (const key of this.steps.keys()) {
313
+ if (key.startsWith(prefix)) {
314
+ const step = this.steps.get(key)
315
+ if (step) {
316
+ this.stepData.delete(step.stepId)
317
+ this.branchKeys.delete(step.stepId)
318
+ }
319
+ this.steps.delete(key)
320
+ }
321
+ }
322
+ return existed
323
+ }
324
+
261
325
  async withRunLock<T>(_id: string, fn: () => Promise<T>): Promise<T> {
262
326
  // In-memory service doesn't need locking for inline execution
263
327
  return fn()
@@ -21,7 +21,10 @@ import {
21
21
  resolveNamespace,
22
22
  ContextAwareRPCService,
23
23
  } from '../../wirings/rpc/rpc-runner.js'
24
- import { buildDynamicWorkflowInstructions, buildWorkflowTools } from './agent-dynamic-workflow.js'
24
+ import {
25
+ buildDynamicWorkflowInstructions,
26
+ buildWorkflowTools,
27
+ } from './agent-dynamic-workflow.js'
25
28
  import {
26
29
  resolveMemoryServices,
27
30
  loadContextMessages,
@@ -122,7 +125,10 @@ export async function buildInstructions(
122
125
  }
123
126
 
124
127
  if (meta?.dynamicWorkflows && meta.tools?.length) {
125
- instructions += buildDynamicWorkflowInstructions(meta.tools, meta.dynamicWorkflows)
128
+ instructions += buildDynamicWorkflowInstructions(
129
+ meta.tools,
130
+ meta.dynamicWorkflows
131
+ )
126
132
  }
127
133
 
128
134
  return instructions
@@ -408,6 +414,23 @@ export async function buildToolDefs(
408
414
  params,
409
415
  agentSessionMap
410
416
  )
417
+ if (
418
+ result.status === 'suspended' &&
419
+ result.pendingApprovals?.length
420
+ ) {
421
+ return {
422
+ __approvalRequired: true,
423
+ toolName: subAgentName,
424
+ args: toolInput,
425
+ agentRunId: result.runId,
426
+ subApprovals: result.pendingApprovals.map((a) => ({
427
+ toolCallId: a.toolCallId,
428
+ toolName: a.toolName,
429
+ args: a.args,
430
+ runId: a.runId,
431
+ })),
432
+ }
433
+ }
411
434
  return result.object ?? result.text
412
435
  },
413
436
  })
@@ -30,6 +30,12 @@ const channelMiddlewareCache: Record<
30
30
  readonly CorePikkuChannelMiddleware[]
31
31
  > = {}
32
32
 
33
+ export const clearChannelMiddlewareCache = () => {
34
+ for (const key of Object.keys(channelMiddlewareCache)) {
35
+ delete channelMiddlewareCache[key]
36
+ }
37
+ }
38
+
33
39
  export const combineChannelMiddleware = (
34
40
  wireType: string,
35
41
  uid: string,
@@ -464,25 +464,28 @@ export abstract class PikkuWorkflowService implements WorkflowService {
464
464
  throw new Error(`Missing workflow graphHash for '${name}'`)
465
465
  }
466
466
 
467
+ const shouldInline =
468
+ options?.inline || !getSingletonServices()?.queueService
469
+
467
470
  const runId = await this.createRun(
468
471
  name,
469
472
  input,
470
- options?.inline ?? false,
473
+ shouldInline,
471
474
  workflowMeta.graphHash,
472
475
  wire
473
476
  )
474
477
 
475
- if (options?.inline) {
478
+ if (shouldInline) {
476
479
  this.inlineRuns.add(runId)
477
- }
478
-
479
- if (options?.inline || !getSingletonServices()?.queueService) {
480
480
  this.runWorkflowJob(runId, rpcService)
481
- .catch(() => {})
481
+ .catch((err) => {
482
+ getSingletonServices()!.logger.error(
483
+ `Workflow ${name} (run ${runId}) failed:`,
484
+ err
485
+ )
486
+ })
482
487
  .finally(() => {
483
- if (options?.inline) {
484
- this.inlineRuns.delete(runId)
485
- }
488
+ this.inlineRuns.delete(runId)
486
489
  })
487
490
  } else {
488
491
  await this.resumeWorkflow(runId)