@pikku/core 0.12.67 → 0.12.70
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 +397 -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-workflow-service.d.ts +2 -2
- package/dist/services/in-memory-workflow-service.js +2 -2
- 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/types/core.types.d.ts +21 -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-prepare.d.ts +18 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +26 -4
- package/dist/wirings/cli/command-parser.js +11 -1
- package/dist/wirings/queue/index.d.ts +1 -1
- package/dist/wirings/queue/queue.types.d.ts +30 -0
- 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/index.d.ts +13 -2
- package/dist/wirings/workflow/index.js +15 -0
- 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 +118 -12
- package/dist/wirings/workflow/pikku-workflow-service.js +166 -153
- 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 +119 -2
- 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-workflow-service.test.ts +50 -1
- package/src/services/in-memory-workflow-service.ts +3 -2
- 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/types/core.types.ts +27 -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-prepare.test.ts +29 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +38 -4
- package/src/wirings/cli/command-parser.test.ts +60 -0
- package/src/wirings/cli/command-parser.ts +12 -1
- package/src/wirings/queue/index.ts +2 -0
- package/src/wirings/queue/queue.types.ts +32 -0
- 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/index.ts +74 -0
- package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +126 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +306 -228
- 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-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 +441 -8
- package/src/wirings/workflow/scenario-step.types.ts +157 -0
- package/src/wirings/workflow/workflow.types.ts +137 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,13 +2,20 @@ import type {
|
|
|
2
2
|
ScenarioActor,
|
|
3
3
|
ScenarioActorConfig,
|
|
4
4
|
ScenarioActors,
|
|
5
|
+
ScenarioInvokeOptions,
|
|
6
|
+
ScenarioHttpResponse,
|
|
5
7
|
} from './scenario-actors-service.js'
|
|
8
|
+
import { readScenarioHttpResponse } from './scenario-actors-service.js'
|
|
6
9
|
import type {
|
|
7
10
|
ConverseOptions,
|
|
8
11
|
ActorFlowVerdict,
|
|
9
12
|
TargetAgentReply,
|
|
10
13
|
} from '../wirings/actor-flow/actor-flow.types.js'
|
|
11
14
|
import { runConversation } from '../wirings/actor-flow/run-conversation.js'
|
|
15
|
+
import {
|
|
16
|
+
createCookieJar,
|
|
17
|
+
type ScenarioCookieJar,
|
|
18
|
+
} from '../wirings/workflow/scenario-cookie-jar.js'
|
|
12
19
|
import { getSingletonServices } from '../pikku-state.js'
|
|
13
20
|
import { AIProviderNotConfiguredError } from '../errors/errors.js'
|
|
14
21
|
|
|
@@ -48,15 +55,21 @@ export interface HttpScenarioActorsConfig {
|
|
|
48
55
|
* outlive a session).
|
|
49
56
|
*/
|
|
50
57
|
export class HttpScenarioActor implements ScenarioActor {
|
|
51
|
-
private
|
|
52
|
-
|
|
58
|
+
private jar: ScenarioCookieJar
|
|
59
|
+
/**
|
|
60
|
+
* Whether `login()` has succeeded since the last time the session was
|
|
61
|
+
* dropped. The jar cannot answer this — a target may set a cookie before
|
|
62
|
+
* anyone signs in, and it would then look like a session that was never
|
|
63
|
+
* established.
|
|
64
|
+
*/
|
|
65
|
+
private signedIn = false
|
|
53
66
|
|
|
54
67
|
constructor(
|
|
55
68
|
readonly name: string,
|
|
56
69
|
private actorConfig: ScenarioActorConfig,
|
|
57
70
|
private config: HttpScenarioActorsConfig
|
|
58
71
|
) {
|
|
59
|
-
this.
|
|
72
|
+
this.jar = createCookieJar(config.apiUrl)
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
get email(): string {
|
|
@@ -64,17 +77,31 @@ export class HttpScenarioActor implements ScenarioActor {
|
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
async invoke(rpcName: string, data: unknown): Promise<unknown> {
|
|
67
|
-
const
|
|
68
|
-
|
|
80
|
+
const res = await this.invokeRaw(rpcName, data)
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`[scenario] '${rpcName}' as '${this.name}' returned ${res.status}: ${res.serialized.slice(0, 300)}`
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
return res.body
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async invokeRaw(
|
|
90
|
+
rpcName: string,
|
|
91
|
+
data: unknown,
|
|
92
|
+
options?: ScenarioInvokeOptions
|
|
93
|
+
): Promise<ScenarioHttpResponse> {
|
|
94
|
+
if (!this.signedIn) {
|
|
95
|
+
await this.login()
|
|
96
|
+
}
|
|
97
|
+
let res = await this.postRpc(rpcName, data, options?.headers)
|
|
69
98
|
if (res.status === 401) {
|
|
70
99
|
// Session expired mid-run — re-login once and retry.
|
|
71
|
-
this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await this.postRpc(rpcName, data, await this.login())
|
|
75
|
-
)
|
|
100
|
+
this.signOut()
|
|
101
|
+
await this.login()
|
|
102
|
+
res = await this.postRpc(rpcName, data, options?.headers)
|
|
76
103
|
}
|
|
77
|
-
return
|
|
104
|
+
return readScenarioHttpResponse(res)
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
async converse(options: ConverseOptions): Promise<ActorFlowVerdict> {
|
|
@@ -92,8 +119,8 @@ export class HttpScenarioActor implements ScenarioActor {
|
|
|
92
119
|
const resourceId = `actor:${this.name}`
|
|
93
120
|
|
|
94
121
|
return runConversation({
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
actor: this.actorConfig,
|
|
123
|
+
actorName: this.actorConfig.name ?? this.name,
|
|
97
124
|
agentName: options.agent,
|
|
98
125
|
task: options.task,
|
|
99
126
|
evaluate: options.evaluate,
|
|
@@ -148,21 +175,18 @@ export class HttpScenarioActor implements ScenarioActor {
|
|
|
148
175
|
private async postAgent(subPath: string, body: unknown): Promise<unknown> {
|
|
149
176
|
const rpcPath = this.config.rpcPath ?? '/rpc'
|
|
150
177
|
const url = `${this.config.apiUrl}${rpcPath}/${subPath}`
|
|
151
|
-
const send = (
|
|
152
|
-
fetch(url, {
|
|
178
|
+
const send = () =>
|
|
179
|
+
this.jar.fetch(url, {
|
|
153
180
|
method: 'POST',
|
|
154
|
-
headers: {
|
|
155
|
-
'content-type': 'application/json',
|
|
156
|
-
origin: this.origin,
|
|
157
|
-
...(cookie ? { cookie } : {}),
|
|
158
|
-
},
|
|
181
|
+
headers: { 'content-type': 'application/json' },
|
|
159
182
|
body: JSON.stringify(body),
|
|
160
183
|
})
|
|
161
184
|
|
|
162
|
-
let res = await send(
|
|
185
|
+
let res = await send()
|
|
163
186
|
if (res.status === 401) {
|
|
164
|
-
this.
|
|
165
|
-
|
|
187
|
+
this.signOut()
|
|
188
|
+
await this.login()
|
|
189
|
+
res = await send()
|
|
166
190
|
}
|
|
167
191
|
if (!res.ok) {
|
|
168
192
|
const text = (await res.text().catch(() => '')).slice(0, 300)
|
|
@@ -175,36 +199,30 @@ export class HttpScenarioActor implements ScenarioActor {
|
|
|
175
199
|
return text ? JSON.parse(text) : undefined
|
|
176
200
|
}
|
|
177
201
|
|
|
178
|
-
private async postRpc(
|
|
202
|
+
private async postRpc(
|
|
203
|
+
rpcName: string,
|
|
204
|
+
data: unknown,
|
|
205
|
+
extraHeaders?: Record<string, string>
|
|
206
|
+
) {
|
|
179
207
|
const rpcPath = this.config.rpcPath ?? '/rpc'
|
|
180
|
-
return fetch(`${this.config.apiUrl}${rpcPath}/${rpcName}`, {
|
|
208
|
+
return this.jar.fetch(`${this.config.apiUrl}${rpcPath}/${rpcName}`, {
|
|
181
209
|
method: 'POST',
|
|
182
|
-
headers: {
|
|
183
|
-
'content-type': 'application/json',
|
|
184
|
-
origin: this.origin,
|
|
185
|
-
cookie,
|
|
186
|
-
},
|
|
210
|
+
headers: { 'content-type': 'application/json', ...extraHeaders },
|
|
187
211
|
body: JSON.stringify({ data }),
|
|
188
212
|
})
|
|
189
213
|
}
|
|
190
214
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`[scenario] '${rpcName}' as '${this.name}' returned ${res.status}: ${body}`
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
if (res.status === 204) return undefined
|
|
199
|
-
const text = await res.text()
|
|
200
|
-
return text ? JSON.parse(text) : undefined
|
|
215
|
+
/** Drop the session, so the next call signs in again before it goes out. */
|
|
216
|
+
private signOut(): void {
|
|
217
|
+
this.jar.clear()
|
|
218
|
+
this.signedIn = false
|
|
201
219
|
}
|
|
202
220
|
|
|
203
|
-
private async login(): Promise<
|
|
221
|
+
private async login(): Promise<void> {
|
|
204
222
|
const signInPath = this.config.signInPath ?? '/auth/sign-in/actor'
|
|
205
|
-
const res = await fetch(`${this.config.apiUrl}${signInPath}`, {
|
|
223
|
+
const res = await this.jar.fetch(`${this.config.apiUrl}${signInPath}`, {
|
|
206
224
|
method: 'POST',
|
|
207
|
-
headers: { 'content-type': 'application/json'
|
|
225
|
+
headers: { 'content-type': 'application/json' },
|
|
208
226
|
body: JSON.stringify({
|
|
209
227
|
email: this.actorConfig.email,
|
|
210
228
|
name: this.actorConfig.name ?? this.name,
|
|
@@ -217,18 +235,14 @@ export class HttpScenarioActor implements ScenarioActor {
|
|
|
217
235
|
`[scenario] actor sign-in failed for '${this.name}' (${res.status}): ${body}`
|
|
218
236
|
)
|
|
219
237
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.filter(Boolean)
|
|
224
|
-
.join('; ')
|
|
225
|
-
if (!cookie) {
|
|
238
|
+
// What proves a session was established is this response setting a cookie,
|
|
239
|
+
// not the jar being non-empty — the target may have set one earlier.
|
|
240
|
+
if (res.headers.getSetCookie().length === 0) {
|
|
226
241
|
throw new Error(
|
|
227
242
|
`[scenario] actor sign-in for '${this.name}' returned no session cookie`
|
|
228
243
|
)
|
|
229
244
|
}
|
|
230
|
-
this.
|
|
231
|
-
return cookie
|
|
245
|
+
this.signedIn = true
|
|
232
246
|
}
|
|
233
247
|
}
|
|
234
248
|
|
|
@@ -2,7 +2,7 @@ import { describe, test, beforeEach } from 'node:test'
|
|
|
2
2
|
import assert from 'node:assert'
|
|
3
3
|
import { InMemoryWorkflowService } from './in-memory-workflow-service.js'
|
|
4
4
|
import { getQueueWorkers } from '../wirings/queue/queue-runner.js'
|
|
5
|
-
import { pikkuState } from '../pikku-state.js'
|
|
5
|
+
import { pikkuState, resetPikkuState } from '../pikku-state.js'
|
|
6
6
|
|
|
7
7
|
let service: InMemoryWorkflowService
|
|
8
8
|
|
|
@@ -390,5 +390,54 @@ describe('InMemoryWorkflowService', () => {
|
|
|
390
390
|
assert.strictEqual(getQueueWorkers().has(stepQueue), true)
|
|
391
391
|
assert.strictEqual(getQueueWorkers().has(orchQueue), true)
|
|
392
392
|
})
|
|
393
|
+
|
|
394
|
+
test('shared-groups leaves per-workflow queues unconsumed', () => {
|
|
395
|
+
const orchQueue = 'wf-orchestrator-shared-strategy-check'
|
|
396
|
+
|
|
397
|
+
const queueMeta = pikkuState(null, 'queue', 'meta')
|
|
398
|
+
queueMeta[orchQueue] = {
|
|
399
|
+
name: orchQueue,
|
|
400
|
+
pikkuFuncId: 'pikkuWorkflowOrchestrator:sharedStrategyCheck',
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const ws = new InMemoryWorkflowService({
|
|
404
|
+
queueStrategy: 'shared-groups',
|
|
405
|
+
})
|
|
406
|
+
ws.wireQueueWorkers()
|
|
407
|
+
|
|
408
|
+
// The whole point: one set of pollers, not one per workflow.
|
|
409
|
+
assert.strictEqual(getQueueWorkers().has(orchQueue), false)
|
|
410
|
+
assert.strictEqual(
|
|
411
|
+
getQueueWorkers().has('pikku-workflow-orchestrator'),
|
|
412
|
+
true
|
|
413
|
+
)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('shared-groups caps how much of the shared queue one workflow takes', () => {
|
|
417
|
+
// registerWorkflowFunc is first-write-wins, so start from clean state
|
|
418
|
+
// rather than inheriting the shared queues an earlier test registered.
|
|
419
|
+
resetPikkuState()
|
|
420
|
+
const ws = new InMemoryWorkflowService({
|
|
421
|
+
queueStrategy: 'shared-groups',
|
|
422
|
+
queueConcurrency: 20,
|
|
423
|
+
queueGroupConcurrency: 3,
|
|
424
|
+
})
|
|
425
|
+
ws.wireQueueWorkers()
|
|
426
|
+
|
|
427
|
+
const worker = getQueueWorkers().get('pikku-workflow-orchestrator')
|
|
428
|
+
assert.deepStrictEqual(worker?.config, {
|
|
429
|
+
batchSize: 20,
|
|
430
|
+
groupConcurrency: 3,
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
test('per-workflow (the default) leaves the shared queues unconstrained', () => {
|
|
435
|
+
resetPikkuState()
|
|
436
|
+
const ws = new InMemoryWorkflowService()
|
|
437
|
+
ws.wireQueueWorkers()
|
|
438
|
+
|
|
439
|
+
const worker = getQueueWorkers().get('pikku-workflow-orchestrator')
|
|
440
|
+
assert.strictEqual(worker?.config, undefined)
|
|
441
|
+
})
|
|
393
442
|
})
|
|
394
443
|
})
|
|
@@ -4,6 +4,7 @@ import { isExpectedError } from '../errors/error-handler.js'
|
|
|
4
4
|
import type { SerializedError } from '../types/core.types.js'
|
|
5
5
|
import type {
|
|
6
6
|
WorkflowPlannedStep,
|
|
7
|
+
WorkflowQueueOptions,
|
|
7
8
|
WorkflowRun,
|
|
8
9
|
WorkflowRunService,
|
|
9
10
|
WorkflowRunWire,
|
|
@@ -39,8 +40,8 @@ export class InMemoryWorkflowService
|
|
|
39
40
|
extends PikkuWorkflowService
|
|
40
41
|
implements WorkflowRunService
|
|
41
42
|
{
|
|
42
|
-
constructor() {
|
|
43
|
-
super({ wireQueues: false })
|
|
43
|
+
constructor(options: WorkflowQueueOptions = {}) {
|
|
44
|
+
super({ ...options, wireQueues: false })
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
private sleepTimers = new Set<ReturnType<typeof setTimeout>>()
|
package/src/services/index.ts
CHANGED
|
@@ -43,8 +43,13 @@ export type {
|
|
|
43
43
|
export type {
|
|
44
44
|
ScenarioActor,
|
|
45
45
|
ScenarioActorConfig,
|
|
46
|
+
ScenarioActorOf,
|
|
46
47
|
ScenarioActors,
|
|
48
|
+
ScenarioInvokeOptions,
|
|
49
|
+
ScenarioRpcMap,
|
|
50
|
+
ScenarioHttpResponse,
|
|
47
51
|
} from './scenario-actors-service.js'
|
|
52
|
+
export { readScenarioHttpResponse } from './scenario-actors-service.js'
|
|
48
53
|
export {
|
|
49
54
|
HttpScenarioActor,
|
|
50
55
|
createHttpScenarioActors,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, test, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { LocalMetaService } from './meta-service.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scenarios and their steps are generated into `.pikku/scenarios/` so that no
|
|
11
|
+
* app-facing module imports them. Everything that reads meta off disk — the
|
|
12
|
+
* console's scenario list among them — must still see them.
|
|
13
|
+
*/
|
|
14
|
+
describe('LocalMetaService reads the scenario meta alongside the app meta', () => {
|
|
15
|
+
let pikkuDir: string
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
pikkuDir = await mkdtemp(join(tmpdir(), 'pikku-meta-'))
|
|
19
|
+
await mkdir(join(pikkuDir, 'workflow', 'meta'), { recursive: true })
|
|
20
|
+
await mkdir(join(pikkuDir, 'scenarios', 'meta'), { recursive: true })
|
|
21
|
+
await mkdir(join(pikkuDir, 'function'), { recursive: true })
|
|
22
|
+
await mkdir(join(pikkuDir, 'scenarios'), { recursive: true })
|
|
23
|
+
|
|
24
|
+
await writeFile(
|
|
25
|
+
join(pikkuDir, 'workflow', 'meta', 'orderWorkflow.gen.json'),
|
|
26
|
+
JSON.stringify({ name: 'orderWorkflow', source: 'dsl' })
|
|
27
|
+
)
|
|
28
|
+
await writeFile(
|
|
29
|
+
join(pikkuDir, 'scenarios', 'meta', 'codeEditorScenario.gen.json'),
|
|
30
|
+
JSON.stringify({ name: 'codeEditorScenario', source: 'scenario' })
|
|
31
|
+
)
|
|
32
|
+
await writeFile(
|
|
33
|
+
join(pikkuDir, 'function', 'pikku-functions-meta.gen.json'),
|
|
34
|
+
JSON.stringify({ createTodo: { pikkuFuncId: 'createTodo' } })
|
|
35
|
+
)
|
|
36
|
+
await writeFile(
|
|
37
|
+
join(pikkuDir, 'scenarios', 'pikku-scenario-functions-meta.gen.json'),
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
opensPage: { pikkuFuncId: 'opensPage', scenarioStep: true },
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
after(async () => {
|
|
45
|
+
await rm(pikkuDir, { recursive: true, force: true })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('a scenario is still a workflow to whoever reads the meta', async () => {
|
|
49
|
+
const meta = await new LocalMetaService(pikkuDir).getWorkflowMeta()
|
|
50
|
+
assert.deepEqual(Object.keys(meta).sort(), [
|
|
51
|
+
'codeEditorScenario',
|
|
52
|
+
'orderWorkflow',
|
|
53
|
+
])
|
|
54
|
+
assert.equal(meta['codeEditorScenario']!.source, 'scenario')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('a scenario step is still a function to whoever reads the meta', async () => {
|
|
58
|
+
const meta = await new LocalMetaService(pikkuDir).getFunctionsMeta()
|
|
59
|
+
assert.deepEqual(Object.keys(meta).sort(), ['createTodo', 'opensPage'])
|
|
60
|
+
assert.equal(meta['opensPage']!.scenarioStep, true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a project with no scenarios reads exactly what it has', async () => {
|
|
64
|
+
const empty = await mkdtemp(join(tmpdir(), 'pikku-meta-empty-'))
|
|
65
|
+
await mkdir(join(empty, 'workflow', 'meta'), { recursive: true })
|
|
66
|
+
await writeFile(
|
|
67
|
+
join(empty, 'workflow', 'meta', 'orderWorkflow.gen.json'),
|
|
68
|
+
JSON.stringify({ name: 'orderWorkflow', source: 'dsl' })
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const service = new LocalMetaService(empty)
|
|
72
|
+
assert.deepEqual(Object.keys(await service.getWorkflowMeta()), [
|
|
73
|
+
'orderWorkflow',
|
|
74
|
+
])
|
|
75
|
+
assert.deepEqual(Object.keys(await service.getFunctionsMeta()), [])
|
|
76
|
+
|
|
77
|
+
await rm(empty, { recursive: true, force: true })
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -13,7 +13,10 @@ import type {
|
|
|
13
13
|
MCPToolMeta,
|
|
14
14
|
MCPPromptMeta,
|
|
15
15
|
} from '../wirings/mcp/mcp.types.js'
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
FeaturesMeta,
|
|
18
|
+
WorkflowsMeta,
|
|
19
|
+
} from '../wirings/workflow/workflow.types.js'
|
|
17
20
|
import type { ScenarioActorConfig } from './scenario-actors-service.js'
|
|
18
21
|
import type {
|
|
19
22
|
TriggerMeta,
|
|
@@ -186,6 +189,7 @@ export interface MetaService {
|
|
|
186
189
|
getRpcMeta(): Promise<RPCMetaRecord>
|
|
187
190
|
getWorkflowMeta(): Promise<WorkflowsMeta>
|
|
188
191
|
getScenarioActorsMeta(): Promise<Record<string, ScenarioActorConfig>>
|
|
192
|
+
getFeaturesMeta(): Promise<FeaturesMeta>
|
|
189
193
|
getTriggerMeta(): Promise<TriggerMeta>
|
|
190
194
|
getTriggerSourceMeta(): Promise<TriggerSourceMeta>
|
|
191
195
|
getFunctionsMeta(): Promise<FunctionsMeta>
|
|
@@ -226,6 +230,7 @@ export class LocalMetaService implements MetaService {
|
|
|
226
230
|
private workflowMetaCache: WorkflowsMeta | null = null
|
|
227
231
|
private scenarioActorsMetaCache: Record<string, ScenarioActorConfig> | null =
|
|
228
232
|
null
|
|
233
|
+
private featuresMetaCache: FeaturesMeta | null = null
|
|
229
234
|
private triggerMetaCache: TriggerMeta | null = null
|
|
230
235
|
private triggerSourceMetaCache: TriggerSourceMeta | null = null
|
|
231
236
|
private functionsMetaCache: FunctionsMeta | null = null
|
|
@@ -328,6 +333,7 @@ export class LocalMetaService implements MetaService {
|
|
|
328
333
|
this.rpcMetaCache = null
|
|
329
334
|
this.workflowMetaCache = null
|
|
330
335
|
this.scenarioActorsMetaCache = null
|
|
336
|
+
this.featuresMetaCache = null
|
|
331
337
|
this.triggerMetaCache = null
|
|
332
338
|
this.triggerSourceMetaCache = null
|
|
333
339
|
this.functionsMetaCache = null
|
|
@@ -456,34 +462,47 @@ export class LocalMetaService implements MetaService {
|
|
|
456
462
|
}
|
|
457
463
|
}
|
|
458
464
|
|
|
465
|
+
private async readWorkflowMetaDir(
|
|
466
|
+
dir: string,
|
|
467
|
+
into: WorkflowsMeta
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
const files = await this.readDir(dir)
|
|
470
|
+
const jsonFiles = files.filter((f) => f.endsWith('.gen.json'))
|
|
471
|
+
const verboseFiles = jsonFiles.filter((f) => f.includes('-verbose'))
|
|
472
|
+
const minimalFiles = jsonFiles.filter((f) => !f.includes('-verbose'))
|
|
473
|
+
const verboseNames = new Set(
|
|
474
|
+
verboseFiles.map((f) => f.replace('-verbose.gen.json', ''))
|
|
475
|
+
)
|
|
476
|
+
const filesToRead = [
|
|
477
|
+
...verboseFiles,
|
|
478
|
+
...minimalFiles.filter(
|
|
479
|
+
(f) => !verboseNames.has(f.replace('.gen.json', ''))
|
|
480
|
+
),
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
await Promise.all(
|
|
484
|
+
filesToRead.map(async (file) => {
|
|
485
|
+
const content = await this.readFile(`${dir}/${file}`)
|
|
486
|
+
if (content) {
|
|
487
|
+
const meta = JSON.parse(content)
|
|
488
|
+
into[meta.name] = meta
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
459
494
|
async getWorkflowMeta(): Promise<WorkflowsMeta> {
|
|
460
495
|
if (this.workflowMetaCache) return this.workflowMetaCache
|
|
461
496
|
|
|
462
497
|
try {
|
|
463
|
-
const files = await this.readDir('workflow/meta')
|
|
464
|
-
const jsonFiles = files.filter((f) => f.endsWith('.gen.json'))
|
|
465
|
-
const verboseFiles = jsonFiles.filter((f) => f.includes('-verbose'))
|
|
466
|
-
const minimalFiles = jsonFiles.filter((f) => !f.includes('-verbose'))
|
|
467
|
-
const verboseNames = new Set(
|
|
468
|
-
verboseFiles.map((f) => f.replace('-verbose.gen.json', ''))
|
|
469
|
-
)
|
|
470
|
-
const filesToRead = [
|
|
471
|
-
...verboseFiles,
|
|
472
|
-
...minimalFiles.filter(
|
|
473
|
-
(f) => !verboseNames.has(f.replace('.gen.json', ''))
|
|
474
|
-
),
|
|
475
|
-
]
|
|
476
|
-
|
|
477
498
|
const result: WorkflowsMeta = {}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
})
|
|
486
|
-
)
|
|
499
|
+
// Scenarios keep their meta in `scenarios/meta` so nothing app-facing
|
|
500
|
+
// imports them, but they are still workflows to anything reading meta off
|
|
501
|
+
// disk — the console's scenario list among them.
|
|
502
|
+
await Promise.all([
|
|
503
|
+
this.readWorkflowMetaDir('workflow/meta', result),
|
|
504
|
+
this.readWorkflowMetaDir('scenarios/meta', result),
|
|
505
|
+
])
|
|
487
506
|
|
|
488
507
|
this.workflowMetaCache = result
|
|
489
508
|
return this.workflowMetaCache
|
|
@@ -502,6 +521,14 @@ export class LocalMetaService implements MetaService {
|
|
|
502
521
|
return this.scenarioActorsMetaCache!
|
|
503
522
|
}
|
|
504
523
|
|
|
524
|
+
async getFeaturesMeta(): Promise<FeaturesMeta> {
|
|
525
|
+
if (this.featuresMetaCache) return this.featuresMetaCache
|
|
526
|
+
|
|
527
|
+
const content = await this.readFile('scenarios/features.gen.json')
|
|
528
|
+
this.featuresMetaCache = content ? JSON.parse(content) : {}
|
|
529
|
+
return this.featuresMetaCache!
|
|
530
|
+
}
|
|
531
|
+
|
|
505
532
|
async getTriggerMeta(): Promise<TriggerMeta> {
|
|
506
533
|
if (this.triggerMetaCache) return this.triggerMetaCache
|
|
507
534
|
|
|
@@ -527,8 +554,16 @@ export class LocalMetaService implements MetaService {
|
|
|
527
554
|
async getFunctionsMeta(): Promise<FunctionsMeta> {
|
|
528
555
|
if (this.functionsMetaCache) return this.functionsMetaCache
|
|
529
556
|
|
|
530
|
-
const content = await
|
|
531
|
-
|
|
557
|
+
const [content, scenarioContent] = await Promise.all([
|
|
558
|
+
this.readMetaJson('function', 'pikku-functions-meta'),
|
|
559
|
+
// Scenario steps register only into the scenario bootstrap, but they are
|
|
560
|
+
// still functions to anything reading meta off disk.
|
|
561
|
+
this.readMetaJson('scenarios', 'pikku-scenario-functions-meta'),
|
|
562
|
+
])
|
|
563
|
+
this.functionsMetaCache = {
|
|
564
|
+
...(content ? JSON.parse(content) : {}),
|
|
565
|
+
...(scenarioContent ? JSON.parse(scenarioContent) : {}),
|
|
566
|
+
}
|
|
532
567
|
return this.functionsMetaCache!
|
|
533
568
|
}
|
|
534
569
|
|