@pikku/core 0.12.69 → 0.12.71

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 (118) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +34 -2
  4. package/dist/function/functions.types.d.ts +27 -0
  5. package/dist/index.d.ts +1 -1
  6. package/dist/internal.d.ts +1 -1
  7. package/dist/internal.js +1 -1
  8. package/dist/pikku-state.js +1 -0
  9. package/dist/services/http-scenario-actors.d.ts +12 -4
  10. package/dist/services/http-scenario-actors.js +47 -45
  11. package/dist/services/in-memory-queue-service.d.ts +6 -0
  12. package/dist/services/in-memory-queue-service.js +8 -1
  13. package/dist/services/in-memory-workflow-service.d.ts +3 -5
  14. package/dist/services/in-memory-workflow-service.js +10 -19
  15. package/dist/services/index.d.ts +2 -1
  16. package/dist/services/index.js +1 -0
  17. package/dist/services/meta-service.d.ts +5 -1
  18. package/dist/services/meta-service.js +44 -18
  19. package/dist/services/scenario-actors-service.d.ts +108 -2
  20. package/dist/services/scenario-actors-service.js +40 -1
  21. package/dist/services/workflow-service.d.ts +7 -5
  22. package/dist/types/core.types.d.ts +28 -3
  23. package/dist/types/state.types.d.ts +3 -1
  24. package/dist/wirings/actor-flow/actor-flow.types.d.ts +1 -1
  25. package/dist/wirings/actor-flow/index.d.ts +1 -1
  26. package/dist/wirings/actor-flow/run-conversation.d.ts +10 -10
  27. package/dist/wirings/actor-flow/run-conversation.js +27 -27
  28. package/dist/wirings/ai-agent/ai-agent-agui.js +0 -8
  29. package/dist/wirings/ai-agent/ai-agent-prepare.js +1 -2
  30. package/dist/wirings/ai-agent/ai-agent.types.d.ts +0 -6
  31. package/dist/wirings/cli/command-parser.js +11 -1
  32. package/dist/wirings/rpc/rpc-runner.js +1 -1
  33. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +52 -3
  34. package/dist/wirings/workflow/feature.d.ts +28 -0
  35. package/dist/wirings/workflow/feature.js +57 -0
  36. package/dist/wirings/workflow/graph/graph-runner.js +3 -2
  37. package/dist/wirings/workflow/graph/graph-validation.d.ts +0 -2
  38. package/dist/wirings/workflow/graph/graph-validation.js +0 -142
  39. package/dist/wirings/workflow/graph/index.d.ts +1 -1
  40. package/dist/wirings/workflow/graph/index.js +1 -1
  41. package/dist/wirings/workflow/index.d.ts +13 -3
  42. package/dist/wirings/workflow/index.js +15 -2
  43. package/dist/wirings/workflow/pikku-scenario-service.d.ts +121 -0
  44. package/dist/wirings/workflow/pikku-scenario-service.js +419 -0
  45. package/dist/wirings/workflow/pikku-workflow-service.d.ts +170 -23
  46. package/dist/wirings/workflow/pikku-workflow-service.js +338 -297
  47. package/dist/wirings/workflow/scenario-cookie-jar.d.ts +29 -0
  48. package/dist/wirings/workflow/scenario-cookie-jar.js +51 -0
  49. package/dist/wirings/workflow/scenario-poll.d.ts +20 -0
  50. package/dist/wirings/workflow/scenario-poll.js +25 -0
  51. package/dist/wirings/workflow/scenario-prose.d.ts +38 -0
  52. package/dist/wirings/workflow/scenario-prose.js +45 -0
  53. package/dist/wirings/workflow/scenario-step-guards.d.ts +16 -0
  54. package/dist/wirings/workflow/scenario-step-guards.js +29 -0
  55. package/dist/wirings/workflow/scenario-step.types.d.ts +148 -0
  56. package/dist/wirings/workflow/scenario-step.types.js +1 -0
  57. package/dist/wirings/workflow/workflow.types.d.ts +82 -8
  58. package/package.json +3 -1
  59. package/src/function/functions.types.ts +32 -0
  60. package/src/index.ts +1 -0
  61. package/src/internal.ts +5 -1
  62. package/src/pikku-state.ts +1 -0
  63. package/src/services/http-scenario-actors.test.ts +85 -1
  64. package/src/services/http-scenario-actors.ts +65 -51
  65. package/src/services/in-memory-queue-service.test.ts +66 -1
  66. package/src/services/in-memory-queue-service.ts +13 -2
  67. package/src/services/in-memory-workflow-service.ts +12 -25
  68. package/src/services/index.ts +5 -0
  69. package/src/services/meta-service.test.ts +79 -0
  70. package/src/services/meta-service.ts +61 -26
  71. package/src/services/scenario-actors-service.ts +157 -2
  72. package/src/services/workflow-service.ts +7 -4
  73. package/src/types/core.types.ts +34 -2
  74. package/src/types/state.types.ts +3 -0
  75. package/src/wirings/actor-flow/actor-flow.types.ts +1 -1
  76. package/src/wirings/actor-flow/index.ts +1 -1
  77. package/src/wirings/actor-flow/run-conversation.test.ts +12 -6
  78. package/src/wirings/actor-flow/run-conversation.ts +36 -41
  79. package/src/wirings/ai-agent/ai-agent-agui.test.ts +0 -16
  80. package/src/wirings/ai-agent/ai-agent-agui.ts +0 -9
  81. package/src/wirings/ai-agent/ai-agent-prepare.ts +1 -2
  82. package/src/wirings/ai-agent/ai-agent.types.ts +0 -7
  83. package/src/wirings/cli/command-parser.test.ts +60 -0
  84. package/src/wirings/cli/command-parser.ts +12 -1
  85. package/src/wirings/rpc/rpc-runner.test.ts +28 -5
  86. package/src/wirings/rpc/rpc-runner.ts +1 -1
  87. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +86 -2
  88. package/src/wirings/workflow/feature.test.ts +131 -0
  89. package/src/wirings/workflow/feature.ts +78 -0
  90. package/src/wirings/workflow/graph/graph-runner.ts +3 -2
  91. package/src/wirings/workflow/graph/graph-validation.test.ts +1 -144
  92. package/src/wirings/workflow/graph/graph-validation.ts +0 -196
  93. package/src/wirings/workflow/graph/index.ts +1 -5
  94. package/src/wirings/workflow/index.ts +73 -6
  95. package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
  96. package/src/wirings/workflow/pikku-workflow-service.test.ts +55 -0
  97. package/src/wirings/workflow/pikku-workflow-service.ts +572 -419
  98. package/src/wirings/workflow/scenario-cookie-jar.test.ts +108 -0
  99. package/src/wirings/workflow/scenario-cookie-jar.ts +65 -0
  100. package/src/wirings/workflow/scenario-expectations.test.ts +153 -0
  101. package/src/wirings/workflow/scenario-hooks.test.ts +212 -0
  102. package/src/wirings/workflow/scenario-poll.test.ts +66 -0
  103. package/src/wirings/workflow/scenario-poll.ts +36 -0
  104. package/src/wirings/workflow/scenario-prose.test.ts +152 -0
  105. package/src/wirings/workflow/scenario-prose.ts +79 -0
  106. package/src/wirings/workflow/scenario-service.test.ts +155 -0
  107. package/src/wirings/workflow/scenario-step-guards.ts +43 -0
  108. package/src/wirings/workflow/scenario-step.test.ts +442 -9
  109. package/src/wirings/workflow/scenario-step.types.ts +157 -0
  110. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  111. package/src/wirings/workflow/workflow-dispatch-payload.test.ts +59 -0
  112. package/src/wirings/workflow/workflow-mirror.test.ts +178 -0
  113. package/src/wirings/workflow/workflow-replay-snapshot.test.ts +139 -0
  114. package/src/wirings/workflow/workflow-run-context.test.ts +177 -0
  115. package/src/wirings/workflow/workflow-run-polling.test.ts +132 -0
  116. package/src/wirings/workflow/workflow-step-ordinal.test.ts +4 -4
  117. package/src/wirings/workflow/workflow.types.ts +99 -5
  118. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,6 @@
1
+ import { readScenarioHttpResponse } from './scenario-actors-service.js';
1
2
  import { runConversation } from '../wirings/actor-flow/run-conversation.js';
3
+ import { createCookieJar, } from '../wirings/workflow/scenario-cookie-jar.js';
2
4
  import { getSingletonServices } from '../pikku-state.js';
3
5
  import { AIProviderNotConfiguredError } from '../errors/errors.js';
4
6
  /**
@@ -13,26 +15,42 @@ export class HttpScenarioActor {
13
15
  name;
14
16
  actorConfig;
15
17
  config;
16
- cookie = null;
17
- origin;
18
+ jar;
19
+ /**
20
+ * Whether `login()` has succeeded since the last time the session was
21
+ * dropped. The jar cannot answer this — a target may set a cookie before
22
+ * anyone signs in, and it would then look like a session that was never
23
+ * established.
24
+ */
25
+ signedIn = false;
18
26
  constructor(name, actorConfig, config) {
19
27
  this.name = name;
20
28
  this.actorConfig = actorConfig;
21
29
  this.config = config;
22
- this.origin = new URL(config.apiUrl).origin;
30
+ this.jar = createCookieJar(config.apiUrl);
23
31
  }
24
32
  get email() {
25
33
  return this.actorConfig.email;
26
34
  }
27
35
  async invoke(rpcName, data) {
28
- const cookie = this.cookie ?? (await this.login());
29
- const res = await this.postRpc(rpcName, data, cookie);
36
+ const res = await this.invokeRaw(rpcName, data);
37
+ if (!res.ok) {
38
+ throw new Error(`[scenario] '${rpcName}' as '${this.name}' returned ${res.status}: ${res.serialized.slice(0, 300)}`);
39
+ }
40
+ return res.body;
41
+ }
42
+ async invokeRaw(rpcName, data, options) {
43
+ if (!this.signedIn) {
44
+ await this.login();
45
+ }
46
+ let res = await this.postRpc(rpcName, data, options?.headers);
30
47
  if (res.status === 401) {
31
48
  // Session expired mid-run — re-login once and retry.
32
- this.cookie = null;
33
- return this.readRpcResponse(rpcName, await this.postRpc(rpcName, data, await this.login()));
49
+ this.signOut();
50
+ await this.login();
51
+ res = await this.postRpc(rpcName, data, options?.headers);
34
52
  }
35
- return this.readRpcResponse(rpcName, res);
53
+ return readScenarioHttpResponse(res);
36
54
  }
37
55
  async converse(options) {
38
56
  const { aiAgentRunner } = getSingletonServices();
@@ -46,8 +64,8 @@ export class HttpScenarioActor {
46
64
  const threadId = globalThis.crypto.randomUUID();
47
65
  const resourceId = `actor:${this.name}`;
48
66
  return runConversation({
49
- persona: this.actorConfig,
50
- personaName: this.actorConfig.name ?? this.name,
67
+ actor: this.actorConfig,
68
+ actorName: this.actorConfig.name ?? this.name,
51
69
  agentName: options.agent,
52
70
  task: options.task,
53
71
  evaluate: options.evaluate,
@@ -88,19 +106,16 @@ export class HttpScenarioActor {
88
106
  async postAgent(subPath, body) {
89
107
  const rpcPath = this.config.rpcPath ?? '/rpc';
90
108
  const url = `${this.config.apiUrl}${rpcPath}/${subPath}`;
91
- const send = (cookie) => fetch(url, {
109
+ const send = () => this.jar.fetch(url, {
92
110
  method: 'POST',
93
- headers: {
94
- 'content-type': 'application/json',
95
- origin: this.origin,
96
- ...(cookie ? { cookie } : {}),
97
- },
111
+ headers: { 'content-type': 'application/json' },
98
112
  body: JSON.stringify(body),
99
113
  });
100
- let res = await send(this.cookie);
114
+ let res = await send();
101
115
  if (res.status === 401) {
102
- this.cookie = null;
103
- res = await send(await this.login());
116
+ this.signOut();
117
+ await this.login();
118
+ res = await send();
104
119
  }
105
120
  if (!res.ok) {
106
121
  const text = (await res.text().catch(() => '')).slice(0, 300);
@@ -111,33 +126,24 @@ export class HttpScenarioActor {
111
126
  const text = await res.text();
112
127
  return text ? JSON.parse(text) : undefined;
113
128
  }
114
- async postRpc(rpcName, data, cookie) {
129
+ async postRpc(rpcName, data, extraHeaders) {
115
130
  const rpcPath = this.config.rpcPath ?? '/rpc';
116
- return fetch(`${this.config.apiUrl}${rpcPath}/${rpcName}`, {
131
+ return this.jar.fetch(`${this.config.apiUrl}${rpcPath}/${rpcName}`, {
117
132
  method: 'POST',
118
- headers: {
119
- 'content-type': 'application/json',
120
- origin: this.origin,
121
- cookie,
122
- },
133
+ headers: { 'content-type': 'application/json', ...extraHeaders },
123
134
  body: JSON.stringify({ data }),
124
135
  });
125
136
  }
126
- async readRpcResponse(rpcName, res) {
127
- if (!res.ok) {
128
- const body = (await res.text().catch(() => '')).slice(0, 300);
129
- throw new Error(`[scenario] '${rpcName}' as '${this.name}' returned ${res.status}: ${body}`);
130
- }
131
- if (res.status === 204)
132
- return undefined;
133
- const text = await res.text();
134
- return text ? JSON.parse(text) : undefined;
137
+ /** Drop the session, so the next call signs in again before it goes out. */
138
+ signOut() {
139
+ this.jar.clear();
140
+ this.signedIn = false;
135
141
  }
136
142
  async login() {
137
143
  const signInPath = this.config.signInPath ?? '/auth/sign-in/actor';
138
- const res = await fetch(`${this.config.apiUrl}${signInPath}`, {
144
+ const res = await this.jar.fetch(`${this.config.apiUrl}${signInPath}`, {
139
145
  method: 'POST',
140
- headers: { 'content-type': 'application/json', origin: this.origin },
146
+ headers: { 'content-type': 'application/json' },
141
147
  body: JSON.stringify({
142
148
  email: this.actorConfig.email,
143
149
  name: this.actorConfig.name ?? this.name,
@@ -148,16 +154,12 @@ export class HttpScenarioActor {
148
154
  const body = (await res.text().catch(() => '')).slice(0, 300);
149
155
  throw new Error(`[scenario] actor sign-in failed for '${this.name}' (${res.status}): ${body}`);
150
156
  }
151
- const setCookies = res.headers.getSetCookie?.() ?? [];
152
- const cookie = setCookies
153
- .map((c) => c.split(';')[0])
154
- .filter(Boolean)
155
- .join('; ');
156
- if (!cookie) {
157
+ // What proves a session was established is this response setting a cookie,
158
+ // not the jar being non-empty — the target may have set one earlier.
159
+ if (res.headers.getSetCookie().length === 0) {
157
160
  throw new Error(`[scenario] actor sign-in for '${this.name}' returned no session cookie`);
158
161
  }
159
- this.cookie = cookie;
160
- return cookie;
162
+ this.signedIn = true;
161
163
  }
162
164
  }
163
165
  /** Normalize an agentRun/agentApprove HTTP response into a TargetAgentReply. */
@@ -5,6 +5,12 @@ import type { QueueService, JobOptions } from '../wirings/queue/queue.types.js';
5
5
  * a real queue — and redelivers a failed job up to `options.attempts` times with
6
6
  * backoff, so a transiently-failing workflow step recovers exactly as it would
7
7
  * on pg-boss/bullmq instead of being silently dropped on its first error.
8
+ *
9
+ * Payloads are JSON round-tripped on the way in, because every real backend
10
+ * puts the job on a wire (SQS body, Redis value, jsonb column) and the worker
11
+ * therefore never sees the caller's live object. Doing it here keeps dev
12
+ * behaviour honest, and keeps the callers — who cannot know which backend they
13
+ * are talking to — from having to serialise defensively.
8
14
  */
9
15
  export declare class InMemoryQueueService implements QueueService {
10
16
  readonly supportsResults = false;
@@ -5,6 +5,12 @@ import { runQueueJob } from '../wirings/queue/queue-runner.js';
5
5
  * a real queue — and redelivers a failed job up to `options.attempts` times with
6
6
  * backoff, so a transiently-failing workflow step recovers exactly as it would
7
7
  * on pg-boss/bullmq instead of being silently dropped on its first error.
8
+ *
9
+ * Payloads are JSON round-tripped on the way in, because every real backend
10
+ * puts the job on a wire (SQS body, Redis value, jsonb column) and the worker
11
+ * therefore never sees the caller's live object. Doing it here keeps dev
12
+ * behaviour honest, and keeps the callers — who cannot know which backend they
13
+ * are talking to — from having to serialise defensively.
8
14
  */
9
15
  export class InMemoryQueueService {
10
16
  supportsResults = false;
@@ -14,12 +20,13 @@ export class InMemoryQueueService {
14
20
  const maxAttempts = Math.max(1, options?.attempts ?? 1);
15
21
  let attemptsMade = 0;
16
22
  const createdAt = new Date();
23
+ const payload = data === undefined ? data : JSON.parse(JSON.stringify(data));
17
24
  const runAttempt = async () => {
18
25
  attemptsMade++;
19
26
  const job = {
20
27
  id: jobId,
21
28
  queueName,
22
- data,
29
+ data: payload,
23
30
  status: () => 'active',
24
31
  metadata: () => ({ attemptsMade, maxAttempts, createdAt }),
25
32
  pikkuUserId: options?.pikkuUserId,
@@ -66,6 +66,9 @@ export declare class InMemoryWorkflowService extends PikkuWorkflowService implem
66
66
  branchKeys: Record<string, string>;
67
67
  }>;
68
68
  getNodesWithoutSteps(runId: string, nodeIds: string[]): Promise<string[]>;
69
+ protected listStepStates(runId: string): Promise<Array<StepState & {
70
+ stepName: string;
71
+ }>>;
69
72
  getStepInstances(runId: string): Promise<Array<{
70
73
  stepName: string;
71
74
  status: StepStatus;
@@ -82,9 +85,4 @@ export declare class InMemoryWorkflowService extends PikkuWorkflowService implem
82
85
  graph: any;
83
86
  source: string;
84
87
  } | null>;
85
- getAIGeneratedWorkflows(agentName?: string): Promise<Array<{
86
- workflowName: string;
87
- graphHash: string;
88
- graph: any;
89
- }>>;
90
88
  }
@@ -324,6 +324,16 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
324
324
  }
325
325
  return nodeIds.filter((id) => !existingSteps.has(id));
326
326
  }
327
+ async listStepStates(runId) {
328
+ const prefix = `${runId}:`;
329
+ const steps = [];
330
+ for (const [key, step] of this.steps.entries()) {
331
+ if (!key.startsWith(prefix))
332
+ continue;
333
+ steps.push({ ...step, stepName: key.substring(prefix.length) });
334
+ }
335
+ return steps;
336
+ }
327
337
  async getStepInstances(runId) {
328
338
  const prefix = `${runId}:`;
329
339
  const instances = [];
@@ -387,23 +397,4 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
387
397
  return null;
388
398
  return { graph: version.graph, source: version.source };
389
399
  }
390
- async getAIGeneratedWorkflows(agentName) {
391
- const results = [];
392
- const prefix = agentName ? `ai:${agentName}:` : 'ai:';
393
- for (const [key, value] of this.workflowVersions) {
394
- if (value.source !== 'ai-agent' || value.status !== 'active')
395
- continue;
396
- const separatorIdx = key.lastIndexOf(':');
397
- const wfName = key.substring(0, separatorIdx);
398
- const hash = key.substring(separatorIdx + 1);
399
- if (wfName.startsWith(prefix)) {
400
- results.push({
401
- workflowName: wfName,
402
- graphHash: hash,
403
- graph: value.graph,
404
- });
405
- }
406
- }
407
- return results;
408
- }
409
400
  }
@@ -19,7 +19,8 @@ export { InMemoryTriggerService } from './in-memory-trigger-service.js';
19
19
  export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
20
20
  export { LocalGatewayService } from './local-gateway-service.js';
21
21
  export type { ContentService, SignContentKeyArgs, SignURLArgs, GetUploadURLArgs, UploadURLResult, BucketKeyArgs, WriteFileArgs, CopyFileArgs, } from './content-service.js';
22
- export type { ScenarioActor, ScenarioActorConfig, ScenarioActors, } from './scenario-actors-service.js';
22
+ export type { ScenarioActor, ScenarioActorConfig, ScenarioActorOf, ScenarioActors, ScenarioInvokeOptions, ScenarioRpcMap, ScenarioHttpResponse, } from './scenario-actors-service.js';
23
+ export { readScenarioHttpResponse } from './scenario-actors-service.js';
23
24
  export { HttpScenarioActor, createHttpScenarioActors, type HttpScenarioActorsConfig, } from './http-scenario-actors.js';
24
25
  export type { JWTService } from './jwt-service.js';
25
26
  export type { EmailService, EmailTemplateReference, SendEmailInput, SendEmailResult, SendHTMLEmailInput, SendTemplateEmailInput, SendTextEmailInput, } from './email-service.js';
@@ -18,6 +18,7 @@ export { InMemoryQueueService } from './in-memory-queue-service.js';
18
18
  export { InMemoryTriggerService } from './in-memory-trigger-service.js';
19
19
  export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
20
20
  export { LocalGatewayService } from './local-gateway-service.js';
21
+ export { readScenarioHttpResponse } from './scenario-actors-service.js';
21
22
  export { HttpScenarioActor, createHttpScenarioActors, } from './http-scenario-actors.js';
22
23
  export { DEFAULT_WEBHOOK_RETRIES, DEFAULT_WEBHOOK_SIGNATURE_HEADER, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, WebhookService, } from './webhook-service.js';
23
24
  export { TypedCredentialService } from './typed-credential-service.js';
@@ -5,7 +5,7 @@ import type { ScheduledTasksMeta } from '../wirings/scheduler/scheduler.types.js
5
5
  import type { QueueWorkersMeta } from '../wirings/queue/queue.types.js';
6
6
  import type { CLIMeta } from '../wirings/cli/cli.types.js';
7
7
  import type { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '../wirings/mcp/mcp.types.js';
8
- import type { WorkflowsMeta } from '../wirings/workflow/workflow.types.js';
8
+ import type { FeaturesMeta, WorkflowsMeta } from '../wirings/workflow/workflow.types.js';
9
9
  import type { ScenarioActorConfig } from './scenario-actors-service.js';
10
10
  import type { TriggerMeta, TriggerSourceMeta } from '../wirings/trigger/trigger.types.js';
11
11
  import type { SecretDefinitionsMeta } from '../wirings/secret/secret.types.js';
@@ -151,6 +151,7 @@ export interface MetaService {
151
151
  getRpcMeta(): Promise<RPCMetaRecord>;
152
152
  getWorkflowMeta(): Promise<WorkflowsMeta>;
153
153
  getScenarioActorsMeta(): Promise<Record<string, ScenarioActorConfig>>;
154
+ getFeaturesMeta(): Promise<FeaturesMeta>;
154
155
  getTriggerMeta(): Promise<TriggerMeta>;
155
156
  getTriggerSourceMeta(): Promise<TriggerSourceMeta>;
156
157
  getFunctionsMeta(): Promise<FunctionsMeta>;
@@ -183,6 +184,7 @@ export declare class LocalMetaService implements MetaService {
183
184
  private rpcMetaCache;
184
185
  private workflowMetaCache;
185
186
  private scenarioActorsMetaCache;
187
+ private featuresMetaCache;
186
188
  private triggerMetaCache;
187
189
  private triggerSourceMetaCache;
188
190
  private functionsMetaCache;
@@ -212,8 +214,10 @@ export declare class LocalMetaService implements MetaService {
212
214
  getMcpMeta(): Promise<MCPMeta>;
213
215
  getGatewayMeta(): Promise<GatewaysMeta>;
214
216
  getRpcMeta(): Promise<RPCMetaRecord>;
217
+ private readWorkflowMetaDir;
215
218
  getWorkflowMeta(): Promise<WorkflowsMeta>;
216
219
  getScenarioActorsMeta(): Promise<Record<string, ScenarioActorConfig>>;
220
+ getFeaturesMeta(): Promise<FeaturesMeta>;
217
221
  getTriggerMeta(): Promise<TriggerMeta>;
218
222
  getTriggerSourceMeta(): Promise<TriggerSourceMeta>;
219
223
  getFunctionsMeta(): Promise<FunctionsMeta>;
@@ -18,6 +18,7 @@ export class LocalMetaService {
18
18
  rpcMetaCache = null;
19
19
  workflowMetaCache = null;
20
20
  scenarioActorsMetaCache = null;
21
+ featuresMetaCache = null;
21
22
  triggerMetaCache = null;
22
23
  triggerSourceMetaCache = null;
23
24
  functionsMetaCache = null;
@@ -115,6 +116,7 @@ export class LocalMetaService {
115
116
  this.rpcMetaCache = null;
116
117
  this.workflowMetaCache = null;
117
118
  this.scenarioActorsMetaCache = null;
119
+ this.featuresMetaCache = null;
118
120
  this.triggerMetaCache = null;
119
121
  this.triggerSourceMetaCache = null;
120
122
  this.functionsMetaCache = null;
@@ -223,27 +225,36 @@ export class LocalMetaService {
223
225
  return this.rpcMetaCache;
224
226
  }
225
227
  }
228
+ async readWorkflowMetaDir(dir, into) {
229
+ const files = await this.readDir(dir);
230
+ const jsonFiles = files.filter((f) => f.endsWith('.gen.json'));
231
+ const verboseFiles = jsonFiles.filter((f) => f.includes('-verbose'));
232
+ const minimalFiles = jsonFiles.filter((f) => !f.includes('-verbose'));
233
+ const verboseNames = new Set(verboseFiles.map((f) => f.replace('-verbose.gen.json', '')));
234
+ const filesToRead = [
235
+ ...verboseFiles,
236
+ ...minimalFiles.filter((f) => !verboseNames.has(f.replace('.gen.json', ''))),
237
+ ];
238
+ await Promise.all(filesToRead.map(async (file) => {
239
+ const content = await this.readFile(`${dir}/${file}`);
240
+ if (content) {
241
+ const meta = JSON.parse(content);
242
+ into[meta.name] = meta;
243
+ }
244
+ }));
245
+ }
226
246
  async getWorkflowMeta() {
227
247
  if (this.workflowMetaCache)
228
248
  return this.workflowMetaCache;
229
249
  try {
230
- const files = await this.readDir('workflow/meta');
231
- const jsonFiles = files.filter((f) => f.endsWith('.gen.json'));
232
- const verboseFiles = jsonFiles.filter((f) => f.includes('-verbose'));
233
- const minimalFiles = jsonFiles.filter((f) => !f.includes('-verbose'));
234
- const verboseNames = new Set(verboseFiles.map((f) => f.replace('-verbose.gen.json', '')));
235
- const filesToRead = [
236
- ...verboseFiles,
237
- ...minimalFiles.filter((f) => !verboseNames.has(f.replace('.gen.json', ''))),
238
- ];
239
250
  const result = {};
240
- await Promise.all(filesToRead.map(async (file) => {
241
- const content = await this.readFile(`workflow/meta/${file}`);
242
- if (content) {
243
- const meta = JSON.parse(content);
244
- result[meta.name] = meta;
245
- }
246
- }));
251
+ // Scenarios keep their meta in `scenarios/meta` so nothing app-facing
252
+ // imports them, but they are still workflows to anything reading meta off
253
+ // disk — the console's scenario list among them.
254
+ await Promise.all([
255
+ this.readWorkflowMetaDir('workflow/meta', result),
256
+ this.readWorkflowMetaDir('scenarios/meta', result),
257
+ ]);
247
258
  this.workflowMetaCache = result;
248
259
  return this.workflowMetaCache;
249
260
  }
@@ -260,6 +271,13 @@ export class LocalMetaService {
260
271
  this.scenarioActorsMetaCache = content ? JSON.parse(content) : {};
261
272
  return this.scenarioActorsMetaCache;
262
273
  }
274
+ async getFeaturesMeta() {
275
+ if (this.featuresMetaCache)
276
+ return this.featuresMetaCache;
277
+ const content = await this.readFile('scenarios/features.gen.json');
278
+ this.featuresMetaCache = content ? JSON.parse(content) : {};
279
+ return this.featuresMetaCache;
280
+ }
263
281
  async getTriggerMeta() {
264
282
  if (this.triggerMetaCache)
265
283
  return this.triggerMetaCache;
@@ -277,8 +295,16 @@ export class LocalMetaService {
277
295
  async getFunctionsMeta() {
278
296
  if (this.functionsMetaCache)
279
297
  return this.functionsMetaCache;
280
- const content = await this.readMetaJson('function', 'pikku-functions-meta');
281
- this.functionsMetaCache = content ? JSON.parse(content) : {};
298
+ const [content, scenarioContent] = await Promise.all([
299
+ this.readMetaJson('function', 'pikku-functions-meta'),
300
+ // Scenario steps register only into the scenario bootstrap, but they are
301
+ // still functions to anything reading meta off disk.
302
+ this.readMetaJson('scenarios', 'pikku-scenario-functions-meta'),
303
+ ]);
304
+ this.functionsMetaCache = {
305
+ ...(content ? JSON.parse(content) : {}),
306
+ ...(scenarioContent ? JSON.parse(scenarioContent) : {}),
307
+ };
282
308
  return this.functionsMetaCache;
283
309
  }
284
310
  async getMiddlewareGroupsMeta() {
@@ -1,12 +1,103 @@
1
1
  import type { ConverseOptions, ActorFlowVerdict } from '../wirings/actor-flow/actor-flow.types.js';
2
+ /**
3
+ * What the transport answered, for a step that treats the status as data.
4
+ *
5
+ * An HTTP response with its body already drained: the stream can only be read
6
+ * once, and a step's return value crosses into the run record, so the response
7
+ * object itself cannot travel. This is the shape every caller ends up with.
8
+ */
9
+ export interface ScenarioHttpResponse<T = unknown> {
10
+ status: number;
11
+ ok: boolean;
12
+ /**
13
+ * The parsed JSON body — or, when the body was not JSON, the raw text it was
14
+ * parsed from, so an HTML error page is still readable rather than lost.
15
+ * `undefined` for an empty response.
16
+ *
17
+ * `T` is a claim the caller makes, not one the transport checked: a step that
18
+ * knows the route's payload names it here instead of casting at every use.
19
+ */
20
+ body: T;
21
+ /**
22
+ * The whole body as text, so an assertion can search it without knowing the
23
+ * payload's shape — and so an error body that is HTML rather than JSON still
24
+ * says what went wrong.
25
+ */
26
+ serialized: string;
27
+ }
28
+ /**
29
+ * Drain a response into the shape a step can carry: the parsed body (an empty
30
+ * one counting as no body at all) alongside the text it was parsed from.
31
+ *
32
+ * `invokeRaw` returns this, and a step that has to reach past an actor — a
33
+ * route with no RPC, an identity no actor can hold — reaches for this rather
34
+ * than writing the same record by hand.
35
+ */
36
+ export declare const readScenarioHttpResponse: <T = unknown>(res: Response) => Promise<ScenarioHttpResponse<T>>;
37
+ /** How to send one JSON request, for `postScenarioJson`. */
38
+ export interface ScenarioJsonRequest {
39
+ /** Serialised as the JSON body. Omit for a request that carries none. */
40
+ body?: unknown;
41
+ /** Sent alongside `content-type: application/json`, and may override it. */
42
+ headers?: Record<string, string>;
43
+ /** Defaults to `POST` — the method every scenario route here answers. */
44
+ method?: string;
45
+ /**
46
+ * The `fetch` to send it with. Pass a `ScenarioCookieJar`'s to keep the
47
+ * session; the global `fetch` otherwise, which is what a step asserting on a
48
+ * sessionless call wants.
49
+ */
50
+ fetch?: typeof fetch;
51
+ }
52
+ /**
53
+ * POST JSON somewhere and report what came back, without throwing on a 4xx/5xx.
54
+ *
55
+ * Every scenario that reaches past an actor was writing this by hand — the same
56
+ * `content-type`, the same `JSON.stringify`, the same drain — and the copies had
57
+ * drifted: some returned `res.json()`, which loses the status and throws
58
+ * outright when the target answers an empty body or an HTML error page. A
59
+ * refusal is the expected outcome of a permissions scenario, so it has to
60
+ * survive as data.
61
+ */
62
+ export declare const postScenarioJson: <T = unknown>(url: string, { body, headers, method, fetch: send, }?: ScenarioJsonRequest) => Promise<ScenarioHttpResponse<T>>;
63
+ /** Per-call transport options. */
64
+ export interface ScenarioInvokeOptions {
65
+ /**
66
+ * Headers to send alongside the actor's own session. This is how a step
67
+ * expresses an identity the actor registry cannot — an impersonation header,
68
+ * or one of the header-shim principals a credential scenario invents.
69
+ */
70
+ headers?: Record<string, string>;
71
+ }
72
+ /**
73
+ * The RPC surface an actor can reach, as name → input/output. A project binds
74
+ * its generated exposed RPC map here; the default leaves every name open, which
75
+ * is what an actor built by hand (or by a third-party driver) gets.
76
+ */
77
+ export type ScenarioRpcMap = Record<string, {
78
+ input: any;
79
+ output: any;
80
+ }>;
81
+ /**
82
+ * The actor a step wire carries, for a project whose actor registry is known.
83
+ * An empty registry keeps the open actor type rather than collapsing to
84
+ * `never` — a project may still build actors itself.
85
+ */
86
+ export type ScenarioActorOf<TActors> = [keyof TActors] extends [never] ? ScenarioActor : TActors[keyof TActors];
2
87
  /** A synthetic user (a user row flagged `actor`) that workflow steps run as over the real transport */
3
- export interface ScenarioActor<TAgentName extends string = string> {
88
+ export interface ScenarioActor<TAgentName extends string = string, TRpcMap extends ScenarioRpcMap = ScenarioRpcMap> {
4
89
  /** Stable actor name (the key in pikku.config.json's actor registry). */
5
90
  readonly name: string;
6
91
  /** The actor's user email — flows use it for invites/lookups. */
7
92
  readonly email: string;
8
93
  /** Invoke an exposed RPC as this actor over the real transport. */
9
- invoke(rpcName: string, data: unknown): Promise<unknown>;
94
+ invoke<TName extends keyof TRpcMap & string>(rpcName: TName, data: TRpcMap[TName]['input']): Promise<TRpcMap[TName]['output']>;
95
+ /**
96
+ * The same call, reporting what the transport answered rather than throwing.
97
+ * A refusal is the expected outcome of a permissions or scopes scenario, and
98
+ * `invoke`'s error truncates the body that names which scope was missing.
99
+ */
100
+ invokeRaw<TName extends keyof TRpcMap & string>(rpcName: TName, data: TRpcMap[TName]['input'], options?: ScenarioInvokeOptions): Promise<ScenarioHttpResponse>;
10
101
  /** Converse with a Pikku AI agent in this actor's persona and return its verdict */
11
102
  converse(options: ConverseOptions<TAgentName>): Promise<ActorFlowVerdict>;
12
103
  }
@@ -16,6 +107,21 @@ export interface ScenarioActorConfig {
16
107
  name?: string;
17
108
  jobTitle?: string;
18
109
  personality?: string;
110
+ /**
111
+ * The persona this body is one of — the KIND of person, declared in
112
+ * `scenarios.personas`. Most personas have exactly one actor and it is
113
+ * materialised for them; a second body of the same persona is what tenant
114
+ * isolation and peer-sharing scenarios are made of.
115
+ */
116
+ persona?: string;
117
+ /**
118
+ * Scopes this actor holds, granted directly rather than through a role, and
119
+ * the roles it belongs to. Pikku carries them; it never applies them — which
120
+ * scope store exists and which roles have been created is the app's own, so
121
+ * the app's seed reads these back off `scenarioActorConfigs` and grants them.
122
+ */
123
+ scopes?: readonly string[];
124
+ roles?: readonly string[];
19
125
  }
20
126
  /** The injected `actors` service: actor name → actor. */
21
127
  export type ScenarioActors = Record<string, ScenarioActor>;
@@ -1 +1,40 @@
1
- export {};
1
+ /**
2
+ * Drain a response into the shape a step can carry: the parsed body (an empty
3
+ * one counting as no body at all) alongside the text it was parsed from.
4
+ *
5
+ * `invokeRaw` returns this, and a step that has to reach past an actor — a
6
+ * route with no RPC, an identity no actor can hold — reaches for this rather
7
+ * than writing the same record by hand.
8
+ */
9
+ export const readScenarioHttpResponse = async (res) => {
10
+ const text = res.status === 204 ? '' : await res.text().catch(() => '');
11
+ return {
12
+ status: res.status,
13
+ ok: res.ok,
14
+ body: (text ? parseJsonBody(text) : undefined),
15
+ serialized: text,
16
+ };
17
+ };
18
+ const parseJsonBody = (text) => {
19
+ try {
20
+ return JSON.parse(text);
21
+ }
22
+ catch {
23
+ return text;
24
+ }
25
+ };
26
+ /**
27
+ * POST JSON somewhere and report what came back, without throwing on a 4xx/5xx.
28
+ *
29
+ * Every scenario that reaches past an actor was writing this by hand — the same
30
+ * `content-type`, the same `JSON.stringify`, the same drain — and the copies had
31
+ * drifted: some returned `res.json()`, which loses the status and throws
32
+ * outright when the target answers an empty body or an HTML error page. A
33
+ * refusal is the expected outcome of a permissions scenario, so it has to
34
+ * survive as data.
35
+ */
36
+ export const postScenarioJson = async (url, { body, headers, method = 'POST', fetch: send = fetch, } = {}) => readScenarioHttpResponse(await send(url, {
37
+ method,
38
+ headers: { 'content-type': 'application/json', ...headers },
39
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
40
+ }));
@@ -35,6 +35,13 @@ export interface WorkflowService {
35
35
  }): Promise<{
36
36
  runId: string;
37
37
  }>;
38
+ /**
39
+ * Start a run and wait for it to end.
40
+ *
41
+ * `pollIntervalMs` is the ceiling on the wait between reads of the run, not a
42
+ * fixed cadence: polling starts far shorter than this and widens towards it,
43
+ * so a run that finishes quickly is not held for a whole interval.
44
+ */
38
45
  runToCompletion<I>(name: string, input: I, rpcService: any, options?: {
39
46
  pollIntervalMs?: number;
40
47
  wire?: WorkflowRunWire;
@@ -60,9 +67,4 @@ export interface WorkflowService {
60
67
  graph: any;
61
68
  source: string;
62
69
  } | null>;
63
- getAIGeneratedWorkflows(agentName?: string): Promise<Array<{
64
- workflowName: string;
65
- graphHash: string;
66
- graph: any;
67
- }>>;
68
70
  }