@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.
- package/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +34 -2
- package/dist/function/functions.types.d.ts +27 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/pikku-state.js +1 -0
- package/dist/services/http-scenario-actors.d.ts +12 -4
- package/dist/services/http-scenario-actors.js +47 -45
- package/dist/services/in-memory-queue-service.d.ts +6 -0
- package/dist/services/in-memory-queue-service.js +8 -1
- package/dist/services/in-memory-workflow-service.d.ts +3 -5
- package/dist/services/in-memory-workflow-service.js +10 -19
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.js +1 -0
- package/dist/services/meta-service.d.ts +5 -1
- package/dist/services/meta-service.js +44 -18
- package/dist/services/scenario-actors-service.d.ts +108 -2
- package/dist/services/scenario-actors-service.js +40 -1
- package/dist/services/workflow-service.d.ts +7 -5
- package/dist/types/core.types.d.ts +28 -3
- package/dist/types/state.types.d.ts +3 -1
- package/dist/wirings/actor-flow/actor-flow.types.d.ts +1 -1
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/actor-flow/run-conversation.d.ts +10 -10
- package/dist/wirings/actor-flow/run-conversation.js +27 -27
- package/dist/wirings/ai-agent/ai-agent-agui.js +0 -8
- package/dist/wirings/ai-agent/ai-agent-prepare.js +1 -2
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +0 -6
- package/dist/wirings/cli/command-parser.js +11 -1
- package/dist/wirings/rpc/rpc-runner.js +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +52 -3
- package/dist/wirings/workflow/feature.d.ts +28 -0
- package/dist/wirings/workflow/feature.js +57 -0
- package/dist/wirings/workflow/graph/graph-runner.js +3 -2
- package/dist/wirings/workflow/graph/graph-validation.d.ts +0 -2
- package/dist/wirings/workflow/graph/graph-validation.js +0 -142
- package/dist/wirings/workflow/graph/index.d.ts +1 -1
- package/dist/wirings/workflow/graph/index.js +1 -1
- package/dist/wirings/workflow/index.d.ts +13 -3
- package/dist/wirings/workflow/index.js +15 -2
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +121 -0
- package/dist/wirings/workflow/pikku-scenario-service.js +419 -0
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +170 -23
- package/dist/wirings/workflow/pikku-workflow-service.js +338 -297
- package/dist/wirings/workflow/scenario-cookie-jar.d.ts +29 -0
- package/dist/wirings/workflow/scenario-cookie-jar.js +51 -0
- package/dist/wirings/workflow/scenario-poll.d.ts +20 -0
- package/dist/wirings/workflow/scenario-poll.js +25 -0
- package/dist/wirings/workflow/scenario-prose.d.ts +38 -0
- package/dist/wirings/workflow/scenario-prose.js +45 -0
- package/dist/wirings/workflow/scenario-step-guards.d.ts +16 -0
- package/dist/wirings/workflow/scenario-step-guards.js +29 -0
- package/dist/wirings/workflow/scenario-step.types.d.ts +148 -0
- package/dist/wirings/workflow/scenario-step.types.js +1 -0
- package/dist/wirings/workflow/workflow.types.d.ts +82 -8
- package/package.json +3 -1
- package/src/function/functions.types.ts +32 -0
- package/src/index.ts +1 -0
- package/src/internal.ts +5 -1
- package/src/pikku-state.ts +1 -0
- package/src/services/http-scenario-actors.test.ts +85 -1
- package/src/services/http-scenario-actors.ts +65 -51
- package/src/services/in-memory-queue-service.test.ts +66 -1
- package/src/services/in-memory-queue-service.ts +13 -2
- package/src/services/in-memory-workflow-service.ts +12 -25
- package/src/services/index.ts +5 -0
- package/src/services/meta-service.test.ts +79 -0
- package/src/services/meta-service.ts +61 -26
- package/src/services/scenario-actors-service.ts +157 -2
- package/src/services/workflow-service.ts +7 -4
- package/src/types/core.types.ts +34 -2
- package/src/types/state.types.ts +3 -0
- package/src/wirings/actor-flow/actor-flow.types.ts +1 -1
- package/src/wirings/actor-flow/index.ts +1 -1
- package/src/wirings/actor-flow/run-conversation.test.ts +12 -6
- package/src/wirings/actor-flow/run-conversation.ts +36 -41
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +0 -16
- package/src/wirings/ai-agent/ai-agent-agui.ts +0 -9
- package/src/wirings/ai-agent/ai-agent-prepare.ts +1 -2
- package/src/wirings/ai-agent/ai-agent.types.ts +0 -7
- package/src/wirings/cli/command-parser.test.ts +60 -0
- package/src/wirings/cli/command-parser.ts +12 -1
- package/src/wirings/rpc/rpc-runner.test.ts +28 -5
- package/src/wirings/rpc/rpc-runner.ts +1 -1
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +86 -2
- package/src/wirings/workflow/feature.test.ts +131 -0
- package/src/wirings/workflow/feature.ts +78 -0
- package/src/wirings/workflow/graph/graph-runner.ts +3 -2
- package/src/wirings/workflow/graph/graph-validation.test.ts +1 -144
- package/src/wirings/workflow/graph/graph-validation.ts +0 -196
- package/src/wirings/workflow/graph/index.ts +1 -5
- package/src/wirings/workflow/index.ts +73 -6
- package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +55 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +572 -419
- package/src/wirings/workflow/scenario-cookie-jar.test.ts +108 -0
- package/src/wirings/workflow/scenario-cookie-jar.ts +65 -0
- package/src/wirings/workflow/scenario-expectations.test.ts +153 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +212 -0
- package/src/wirings/workflow/scenario-poll.test.ts +66 -0
- package/src/wirings/workflow/scenario-poll.ts +36 -0
- package/src/wirings/workflow/scenario-prose.test.ts +152 -0
- package/src/wirings/workflow/scenario-prose.ts +79 -0
- package/src/wirings/workflow/scenario-service.test.ts +155 -0
- package/src/wirings/workflow/scenario-step-guards.ts +43 -0
- package/src/wirings/workflow/scenario-step.test.ts +442 -9
- package/src/wirings/workflow/scenario-step.types.ts +157 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
- package/src/wirings/workflow/workflow-dispatch-payload.test.ts +59 -0
- package/src/wirings/workflow/workflow-mirror.test.ts +178 -0
- package/src/wirings/workflow/workflow-replay-snapshot.test.ts +139 -0
- package/src/wirings/workflow/workflow-run-context.test.ts +177 -0
- package/src/wirings/workflow/workflow-run-polling.test.ts +132 -0
- package/src/wirings/workflow/workflow-step-ordinal.test.ts +4 -4
- package/src/wirings/workflow/workflow.types.ts +99 -5
- 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
|
-
|
|
17
|
-
|
|
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.
|
|
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
|
|
29
|
-
|
|
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.
|
|
33
|
-
|
|
49
|
+
this.signOut();
|
|
50
|
+
await this.login();
|
|
51
|
+
res = await this.postRpc(rpcName, data, options?.headers);
|
|
34
52
|
}
|
|
35
|
-
return
|
|
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
|
-
|
|
50
|
-
|
|
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 = (
|
|
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(
|
|
114
|
+
let res = await send();
|
|
101
115
|
if (res.status === 401) {
|
|
102
|
-
this.
|
|
103
|
-
|
|
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,
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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'
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
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
|
}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/services/index.js
CHANGED
|
@@ -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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
281
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|