@pikku/core 0.12.51 → 0.12.53
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 +74 -0
- package/dist/services/http-scenario-actors.d.ts +67 -0
- package/dist/services/http-scenario-actors.js +193 -0
- package/dist/services/http-user-flow-actors.d.ts +20 -0
- package/dist/services/http-user-flow-actors.js +100 -0
- package/dist/services/index.d.ts +5 -2
- package/dist/services/index.js +4 -1
- package/dist/services/istanbul-coverage-service.d.ts +12 -0
- package/dist/services/istanbul-coverage-service.js +41 -0
- package/dist/services/meta-service.d.ts +4 -4
- package/dist/services/meta-service.js +8 -8
- package/dist/services/scenario-actors-service.d.ts +21 -0
- package/dist/services/scenario-actors-service.js +1 -0
- package/dist/services/stub-tracker.d.ts +43 -0
- package/dist/services/stub-tracker.js +146 -0
- package/dist/services/user-flow-actors-service.d.ts +11 -1
- package/dist/services/v8-coverage-service.d.ts +41 -0
- package/dist/services/v8-coverage-service.js +63 -0
- package/dist/types/core.types.d.ts +13 -4
- package/dist/wirings/actor-flow/actor-flow.types.d.ts +68 -0
- package/dist/wirings/actor-flow/actor-flow.types.js +1 -0
- package/dist/wirings/actor-flow/index.d.ts +11 -0
- package/dist/wirings/actor-flow/index.js +1 -0
- package/dist/wirings/actor-flow/run-conversation.d.ts +36 -0
- package/dist/wirings/actor-flow/run-conversation.js +181 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +22 -6
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +4 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +92 -2
- package/dist/wirings/workflow/workflow.types.d.ts +7 -7
- package/package.json +2 -1
- package/src/services/http-scenario-actors-converse.test.ts +222 -0
- package/src/services/{http-user-flow-actors.test.ts → http-scenario-actors.test.ts} +17 -7
- package/src/services/http-scenario-actors.ts +269 -0
- package/src/services/index.ts +27 -8
- package/src/services/istanbul-coverage-service.test.ts +66 -0
- package/src/services/istanbul-coverage-service.ts +65 -0
- package/src/services/meta-service.ts +9 -9
- package/src/services/scenario-actors-service.ts +27 -0
- package/src/services/stub-tracker.test.ts +104 -0
- package/src/services/stub-tracker.ts +185 -0
- package/src/services/v8-coverage-service.test.ts +69 -0
- package/src/services/v8-coverage-service.ts +121 -0
- package/src/types/core.types.ts +13 -4
- package/src/wirings/actor-flow/actor-flow.types.ts +73 -0
- package/src/wirings/actor-flow/index.ts +22 -0
- package/src/wirings/actor-flow/run-conversation.test.ts +176 -0
- package/src/wirings/actor-flow/run-conversation.ts +285 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +144 -5
- package/src/wirings/workflow/{user-flow-step.test.ts → scenario-step.test.ts} +19 -14
- package/src/wirings/workflow/workflow.types.ts +8 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/src/services/http-user-flow-actors.ts +0 -132
- package/src/services/user-flow-actors-service.ts +0 -31
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
## 0.12.53
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- efb0406: Add in-process V8 precise coverage (`pikku dev --coverage` / `pikku serve --coverage`) with per-scenario attribution.
|
|
6
|
+
- `@pikku/core`: new `V8CoverageService` (node:inspector precise coverage with snapshot + reset), exposed as the optional `coverageService` singleton service.
|
|
7
|
+
- `@pikku/inspector`: function meta now records `bodyStart`/`bodyEnd` body spans (verbose meta only) so coverage can be mapped without a runtime TypeScript dependency.
|
|
8
|
+
- `@pikku/cli`: `--coverage` on `pikku dev` and `pikku serve` starts the collector in-process; `pikku scenario run --coverage` resets/snapshots the server between flows and writes `.pikku/coverage/scenario-coverage.json` with per-scenario function coverage.
|
|
9
|
+
- `@pikku/addon-console`: new exposed `takeLiveCoverage` / `resetLiveCoverage` RPCs; V8 ranges are mapped through inline source maps to original TypeScript lines (offset-based, so esbuild/tsx single-line output keeps full resolution).
|
|
10
|
+
|
|
11
|
+
- fe4f5ca: Add `stub`/`spy`/`isTestRun` core utils with call recording for scenario assertions.
|
|
12
|
+
- `@pikku/core`: `StubTracker` moves here from `@pikku/cucumber` (which re-exports it), gaining `record`/`getCalls`/`reset`. New plain-import utils backed by a process-wide tracker: `stub(name, impl?)` (recording fake), `spy(name, real)` (record + pass through), `isTestRun()` (reads `PIKKU_TEST_RUN`). Nothing is injected into service factories and no new factory types exist — swap services with a plain `isTestRun()` conditional where needed. New scenario DSL steps: `workflow.expectService('email.send', { calledWith })` asserts recorded stub calls via the console RPC, `workflow.expectError(...)` walks error branches.
|
|
13
|
+
- `@pikku/cli`: `pikku dev --test` sets `PIKKU_TEST_RUN` and wraps the dev-provided default services (email) in recording spies; independent of `--coverage`, absent from production `pikku serve`. `pikku scenario run` resets recorded calls per flow.
|
|
14
|
+
- `@pikku/addon-console`: exposed `getStubCalls` / `resetStubs` RPCs next to the coverage snapshot endpoints.
|
|
15
|
+
|
|
16
|
+
## 0.12.52
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 61c9ce9: Add `actor.converse(...)` — actor agents for user journeys (#850)
|
|
21
|
+
|
|
22
|
+
An actor can now hold a dynamic, LLM-driven conversation with a target Pikku AI
|
|
23
|
+
agent in its own persona:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const verdict = await actors.pm.converse({
|
|
27
|
+
agent: 'todoBot',
|
|
28
|
+
task: 'Get a todo created for the launch',
|
|
29
|
+
evaluate: 'A todo about the launch now exists',
|
|
30
|
+
})
|
|
31
|
+
// verdict: { passed, reasoning, transcript }
|
|
32
|
+
// then assert deterministically as the same actor:
|
|
33
|
+
const todos = await actors.pm.invoke('listTodos', {})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The actor drives the target over the real transport (the agent's own
|
|
37
|
+
`agentRun` / `agentApprove` HTTP routes, signed in as the actor), plays the
|
|
38
|
+
persona from its `pikku.config.json` config, answers the agent's tool-approval
|
|
39
|
+
requests in-persona (`approvals: 'in-persona' | 'always' | 'never'`), and
|
|
40
|
+
returns its verdict on whether the task was met. Deterministic checks stay the
|
|
41
|
+
caller's job — they already hold the actor.
|
|
42
|
+
|
|
43
|
+
The conversation engine is transport-agnostic (persona LLM + injected target
|
|
44
|
+
driver); the persona's own turns run in-process via the configured
|
|
45
|
+
`aiAgentRunner` (`model` from the call or the actors-service default).
|
|
46
|
+
|
|
47
|
+
`agent` is typed against the generated agent-name union (`keyof AgentMap`), so
|
|
48
|
+
it's author-time checked and autocompleted in a typed project.
|
|
49
|
+
|
|
50
|
+
- f1f39f8: Bound the actor-flow approval loop (#850)
|
|
51
|
+
|
|
52
|
+
`converseWithTarget` now caps suspend→approve rounds within a single target turn
|
|
53
|
+
(default 16, override via `maxApprovalRounds`). A cooperative target completes
|
|
54
|
+
after a handful of rounds; a buggy or uncooperative one — e.g. re-requesting a
|
|
55
|
+
tool the persona keeps denying — previously could spin the inner loop forever
|
|
56
|
+
without ever spending a `maxTurns` credit. Exceeding the cap now throws instead
|
|
57
|
+
of hanging.
|
|
58
|
+
|
|
59
|
+
- c45e98d: Run user flows from the console, actors and all (#850)
|
|
60
|
+
|
|
61
|
+
Starting a `user-flow` workflow without explicit run actors (as the console's
|
|
62
|
+
Run button does) now auto-builds HTTP actors from `USER_FLOW_ACTOR_SECRET` and
|
|
63
|
+
`API_URL`: each actor signs in via the actor auth plugin — which mints the
|
|
64
|
+
`actor: true` user row on first sign-in — and drives its steps over HTTP as
|
|
65
|
+
that persona. When the secret or API base URL isn't configured the run simply
|
|
66
|
+
proceeds without actors (with a warning) instead of failing.
|
|
67
|
+
|
|
68
|
+
The workflow-detail view also gains the shared console header: the workflow
|
|
69
|
+
selector and the "complex workflow" note now live in the header bar, the right
|
|
70
|
+
details panel hides when it has nothing to show, and step nodes display their
|
|
71
|
+
DSL labels (e.g. `Double ${item}`).
|
|
72
|
+
|
|
73
|
+
- 472a349: Rename the userflow concept to scenario (#862). `pikkuUserFlow` becomes `pikkuScenario`, `pikku userflow run/list` becomes `pikku scenario run/list`, the workflow meta flag `userFlow` becomes `scenario`, actor types are now `ScenarioActor`/`ScenarioActors`/`ScenarioActorConfig` (`createHttpScenarioActors`), pikku.config.json's `userFlows` key becomes `scenarios`, the generated actors file is `pikku-scenario-actors.gen.ts` (`createScenarioActors`), the actor sign-in secret env var is `SCENARIO_ACTOR_SECRET`, and the console's User Flows view is now Scenarios.
|
|
74
|
+
|
|
1
75
|
## 0.12.51
|
|
2
76
|
|
|
3
77
|
### Patch Changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ScenarioActor, ScenarioActorConfig, ScenarioActors } from './scenario-actors-service.js';
|
|
2
|
+
import type { ConverseOptions, ActorFlowVerdict } from '../wirings/actor-flow/actor-flow.types.js';
|
|
3
|
+
export interface HttpScenarioActorsConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Base API URL of the target app, INCLUDING the HTTP prefix — e.g.
|
|
6
|
+
* `https://app.example.com/api` or `http://localhost:4000/api`. Actor
|
|
7
|
+
* sign-in is reached at `${apiUrl}${signInPath}` and exposed RPCs at
|
|
8
|
+
* `${apiUrl}${rpcPath}/:rpcName`.
|
|
9
|
+
*/
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
/**
|
|
12
|
+
* The actor impersonation secret. Sign-in only ever works for user rows
|
|
13
|
+
* flagged `actor: true` — knowing the secret never impersonates real users.
|
|
14
|
+
*/
|
|
15
|
+
secret: string;
|
|
16
|
+
/** Actor name → config (usually from pikku.config.json's actor registry). */
|
|
17
|
+
actors: Record<string, ScenarioActorConfig>;
|
|
18
|
+
/** Sign-in path under apiUrl. Default: the actor plugin's `/auth/sign-in/actor`. */
|
|
19
|
+
signInPath?: string;
|
|
20
|
+
/** Exposed-RPC path prefix under apiUrl. Default `/rpc`. */
|
|
21
|
+
rpcPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Default model the persona uses when `actor.converse(...)` is called without
|
|
24
|
+
* an explicit `model`. The persona's own turns/approvals/evaluation run
|
|
25
|
+
* in-process via the configured `aiAgentRunner`.
|
|
26
|
+
*/
|
|
27
|
+
model?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Default HTTP-backed actor. Signs in lazily on first invoke via the Better
|
|
31
|
+
* Auth actor plugin (`POST /auth/sign-in/actor` with `{ email, secret }` —
|
|
32
|
+
* the plugin upserts the actor-flagged user row and mints a session whose
|
|
33
|
+
* `actor` flag flows into audits/analytics). Holds the session cookies for
|
|
34
|
+
* its lifetime; a 401 mid-run re-logs-in once (long health-check runs can
|
|
35
|
+
* outlive a session).
|
|
36
|
+
*/
|
|
37
|
+
export declare class HttpScenarioActor implements ScenarioActor {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
private actorConfig;
|
|
40
|
+
private config;
|
|
41
|
+
private cookie;
|
|
42
|
+
private origin;
|
|
43
|
+
constructor(name: string, actorConfig: ScenarioActorConfig, config: HttpScenarioActorsConfig);
|
|
44
|
+
get email(): string;
|
|
45
|
+
invoke(rpcName: string, data: unknown): Promise<unknown>;
|
|
46
|
+
converse(options: ConverseOptions): Promise<ActorFlowVerdict>;
|
|
47
|
+
/** Start/continue the target agent's run over HTTP as this actor. */
|
|
48
|
+
private agentRun;
|
|
49
|
+
/** Answer the target agent's pending approvals over HTTP and continue. */
|
|
50
|
+
private agentApprove;
|
|
51
|
+
/**
|
|
52
|
+
* POST an agent HTTP route (raw body, not RPC-wrapped). Signs in lazily: the
|
|
53
|
+
* first call goes out with whatever cookie we hold (none, for a no-auth
|
|
54
|
+
* agent), and only a 401 triggers `login()` + one retry. This lets an actor
|
|
55
|
+
* converse with a no-auth agent without any sign-in wiring, while still
|
|
56
|
+
* authenticating against agents that require a session.
|
|
57
|
+
*/
|
|
58
|
+
private postAgent;
|
|
59
|
+
private postRpc;
|
|
60
|
+
private readRpcResponse;
|
|
61
|
+
private login;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build the injected `actors` service from the config registry: actor name →
|
|
65
|
+
* lazy HTTP actor. Wire the result as the `actors` singleton service.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createHttpScenarioActors(config: HttpScenarioActorsConfig): ScenarioActors;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { runConversation } from '../wirings/actor-flow/run-conversation.js';
|
|
2
|
+
import { getSingletonServices } from '../pikku-state.js';
|
|
3
|
+
import { AIProviderNotConfiguredError } from '../errors/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default HTTP-backed actor. Signs in lazily on first invoke via the Better
|
|
6
|
+
* Auth actor plugin (`POST /auth/sign-in/actor` with `{ email, secret }` —
|
|
7
|
+
* the plugin upserts the actor-flagged user row and mints a session whose
|
|
8
|
+
* `actor` flag flows into audits/analytics). Holds the session cookies for
|
|
9
|
+
* its lifetime; a 401 mid-run re-logs-in once (long health-check runs can
|
|
10
|
+
* outlive a session).
|
|
11
|
+
*/
|
|
12
|
+
export class HttpScenarioActor {
|
|
13
|
+
name;
|
|
14
|
+
actorConfig;
|
|
15
|
+
config;
|
|
16
|
+
cookie = null;
|
|
17
|
+
origin;
|
|
18
|
+
constructor(name, actorConfig, config) {
|
|
19
|
+
this.name = name;
|
|
20
|
+
this.actorConfig = actorConfig;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.origin = new URL(config.apiUrl).origin;
|
|
23
|
+
}
|
|
24
|
+
get email() {
|
|
25
|
+
return this.actorConfig.email;
|
|
26
|
+
}
|
|
27
|
+
async invoke(rpcName, data) {
|
|
28
|
+
const cookie = this.cookie ?? (await this.login());
|
|
29
|
+
const res = await this.postRpc(rpcName, data, cookie);
|
|
30
|
+
if (res.status === 401) {
|
|
31
|
+
// 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()));
|
|
34
|
+
}
|
|
35
|
+
return this.readRpcResponse(rpcName, res);
|
|
36
|
+
}
|
|
37
|
+
async converse(options) {
|
|
38
|
+
const { aiAgentRunner } = getSingletonServices();
|
|
39
|
+
if (!aiAgentRunner) {
|
|
40
|
+
throw new AIProviderNotConfiguredError();
|
|
41
|
+
}
|
|
42
|
+
const model = options.model ?? this.config.model;
|
|
43
|
+
if (!model) {
|
|
44
|
+
throw new Error(`[scenario] actor '${this.name}' converse needs a model — pass options.model or set 'model' on the actors service`);
|
|
45
|
+
}
|
|
46
|
+
const threadId = globalThis.crypto.randomUUID();
|
|
47
|
+
const resourceId = `actor:${this.name}`;
|
|
48
|
+
return runConversation({
|
|
49
|
+
persona: this.actorConfig,
|
|
50
|
+
personaName: this.actorConfig.name ?? this.name,
|
|
51
|
+
agentName: options.agent,
|
|
52
|
+
task: options.task,
|
|
53
|
+
evaluate: options.evaluate,
|
|
54
|
+
approvals: options.approvals,
|
|
55
|
+
model,
|
|
56
|
+
maxTurns: options.maxTurns,
|
|
57
|
+
llm: (params) => aiAgentRunner.run(params),
|
|
58
|
+
target: {
|
|
59
|
+
run: (message) => this.agentRun(options.agent, message, threadId, resourceId),
|
|
60
|
+
approve: (runId, decisions) => this.agentApprove(options.agent, runId, decisions),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Start/continue the target agent's run over HTTP as this actor. */
|
|
65
|
+
async agentRun(agentName, message, threadId, resourceId) {
|
|
66
|
+
const raw = await this.postAgent(`agent/${agentName}`, {
|
|
67
|
+
message,
|
|
68
|
+
threadId,
|
|
69
|
+
resourceId,
|
|
70
|
+
});
|
|
71
|
+
return normalizeAgentReply(raw);
|
|
72
|
+
}
|
|
73
|
+
/** Answer the target agent's pending approvals over HTTP and continue. */
|
|
74
|
+
async agentApprove(agentName, runId, decisions) {
|
|
75
|
+
const raw = await this.postAgent(`agent/${agentName}/approve`, {
|
|
76
|
+
runId,
|
|
77
|
+
approvals: decisions,
|
|
78
|
+
});
|
|
79
|
+
return normalizeAgentReply(raw);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* POST an agent HTTP route (raw body, not RPC-wrapped). Signs in lazily: the
|
|
83
|
+
* first call goes out with whatever cookie we hold (none, for a no-auth
|
|
84
|
+
* agent), and only a 401 triggers `login()` + one retry. This lets an actor
|
|
85
|
+
* converse with a no-auth agent without any sign-in wiring, while still
|
|
86
|
+
* authenticating against agents that require a session.
|
|
87
|
+
*/
|
|
88
|
+
async postAgent(subPath, body) {
|
|
89
|
+
const rpcPath = this.config.rpcPath ?? '/rpc';
|
|
90
|
+
const url = `${this.config.apiUrl}${rpcPath}/${subPath}`;
|
|
91
|
+
const send = (cookie) => fetch(url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'content-type': 'application/json',
|
|
95
|
+
origin: this.origin,
|
|
96
|
+
...(cookie ? { cookie } : {}),
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
});
|
|
100
|
+
let res = await send(this.cookie);
|
|
101
|
+
if (res.status === 401) {
|
|
102
|
+
this.cookie = null;
|
|
103
|
+
res = await send(await this.login());
|
|
104
|
+
}
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const text = (await res.text().catch(() => '')).slice(0, 300);
|
|
107
|
+
throw new Error(`[scenario] agent call '${subPath}' as '${this.name}' returned ${res.status}: ${text}`);
|
|
108
|
+
}
|
|
109
|
+
if (res.status === 204)
|
|
110
|
+
return undefined;
|
|
111
|
+
const text = await res.text();
|
|
112
|
+
return text ? JSON.parse(text) : undefined;
|
|
113
|
+
}
|
|
114
|
+
async postRpc(rpcName, data, cookie) {
|
|
115
|
+
const rpcPath = this.config.rpcPath ?? '/rpc';
|
|
116
|
+
return fetch(`${this.config.apiUrl}${rpcPath}/${rpcName}`, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: {
|
|
119
|
+
'content-type': 'application/json',
|
|
120
|
+
origin: this.origin,
|
|
121
|
+
cookie,
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({ data }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
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;
|
|
135
|
+
}
|
|
136
|
+
async login() {
|
|
137
|
+
const signInPath = this.config.signInPath ?? '/auth/sign-in/actor';
|
|
138
|
+
const res = await fetch(`${this.config.apiUrl}${signInPath}`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'content-type': 'application/json', origin: this.origin },
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
email: this.actorConfig.email,
|
|
143
|
+
name: this.actorConfig.name ?? this.name,
|
|
144
|
+
secret: this.config.secret,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
const body = (await res.text().catch(() => '')).slice(0, 300);
|
|
149
|
+
throw new Error(`[scenario] actor sign-in failed for '${this.name}' (${res.status}): ${body}`);
|
|
150
|
+
}
|
|
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
|
+
throw new Error(`[scenario] actor sign-in for '${this.name}' returned no session cookie`);
|
|
158
|
+
}
|
|
159
|
+
this.cookie = cookie;
|
|
160
|
+
return cookie;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Normalize an agentRun/agentApprove HTTP response into a TargetAgentReply. */
|
|
164
|
+
function normalizeAgentReply(raw) {
|
|
165
|
+
const r = (raw ?? {});
|
|
166
|
+
const pending = Array.isArray(r.pendingApprovals)
|
|
167
|
+
? r.pendingApprovals.map((p) => ({
|
|
168
|
+
toolCallId: String(p.toolCallId),
|
|
169
|
+
toolName: String(p.toolName),
|
|
170
|
+
args: p.args,
|
|
171
|
+
reason: typeof p.reason === 'string' ? p.reason : undefined,
|
|
172
|
+
}))
|
|
173
|
+
: undefined;
|
|
174
|
+
return {
|
|
175
|
+
text: typeof r.text === 'string' ? r.text : '',
|
|
176
|
+
runId: typeof r.runId === 'string' ? r.runId : '',
|
|
177
|
+
status: r.status === 'completed' || r.status === 'suspended'
|
|
178
|
+
? r.status
|
|
179
|
+
: undefined,
|
|
180
|
+
pendingApprovals: pending,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Build the injected `actors` service from the config registry: actor name →
|
|
185
|
+
* lazy HTTP actor. Wire the result as the `actors` singleton service.
|
|
186
|
+
*/
|
|
187
|
+
export function createHttpScenarioActors(config) {
|
|
188
|
+
const actors = {};
|
|
189
|
+
for (const [name, actorConfig] of Object.entries(config.actors)) {
|
|
190
|
+
actors[name] = new HttpScenarioActor(name, actorConfig, config);
|
|
191
|
+
}
|
|
192
|
+
return actors;
|
|
193
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UserFlowActor, UserFlowActorConfig, UserFlowActors } from './user-flow-actors-service.js';
|
|
2
|
+
import type { ConverseOptions, ActorFlowVerdict } from '../wirings/actor-flow/actor-flow.types.js';
|
|
2
3
|
export interface HttpUserFlowActorsConfig {
|
|
3
4
|
/**
|
|
4
5
|
* Base API URL of the target app, INCLUDING the HTTP prefix — e.g.
|
|
@@ -18,6 +19,12 @@ export interface HttpUserFlowActorsConfig {
|
|
|
18
19
|
signInPath?: string;
|
|
19
20
|
/** Exposed-RPC path prefix under apiUrl. Default `/rpc`. */
|
|
20
21
|
rpcPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Default model the persona uses when `actor.converse(...)` is called without
|
|
24
|
+
* an explicit `model`. The persona's own turns/approvals/evaluation run
|
|
25
|
+
* in-process via the configured `aiAgentRunner`.
|
|
26
|
+
*/
|
|
27
|
+
model?: string;
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
30
|
* Default HTTP-backed actor. Signs in lazily on first invoke via the Better
|
|
@@ -36,6 +43,19 @@ export declare class HttpUserFlowActor implements UserFlowActor {
|
|
|
36
43
|
constructor(name: string, actorConfig: UserFlowActorConfig, config: HttpUserFlowActorsConfig);
|
|
37
44
|
get email(): string;
|
|
38
45
|
invoke(rpcName: string, data: unknown): Promise<unknown>;
|
|
46
|
+
converse(options: ConverseOptions): Promise<ActorFlowVerdict>;
|
|
47
|
+
/** Start/continue the target agent's run over HTTP as this actor. */
|
|
48
|
+
private agentRun;
|
|
49
|
+
/** Answer the target agent's pending approvals over HTTP and continue. */
|
|
50
|
+
private agentApprove;
|
|
51
|
+
/**
|
|
52
|
+
* POST an agent HTTP route (raw body, not RPC-wrapped). Signs in lazily: the
|
|
53
|
+
* first call goes out with whatever cookie we hold (none, for a no-auth
|
|
54
|
+
* agent), and only a 401 triggers `login()` + one retry. This lets an actor
|
|
55
|
+
* converse with a no-auth agent without any sign-in wiring, while still
|
|
56
|
+
* authenticating against agents that require a session.
|
|
57
|
+
*/
|
|
58
|
+
private postAgent;
|
|
39
59
|
private postRpc;
|
|
40
60
|
private readRpcResponse;
|
|
41
61
|
private login;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { runConversation } from '../wirings/actor-flow/run-conversation.js';
|
|
2
|
+
import { getSingletonServices } from '../pikku-state.js';
|
|
3
|
+
import { AIProviderNotConfiguredError } from '../errors/errors.js';
|
|
1
4
|
/**
|
|
2
5
|
* Default HTTP-backed actor. Signs in lazily on first invoke via the Better
|
|
3
6
|
* Auth actor plugin (`POST /auth/sign-in/actor` with `{ email, secret }` —
|
|
@@ -31,6 +34,83 @@ export class HttpUserFlowActor {
|
|
|
31
34
|
}
|
|
32
35
|
return this.readRpcResponse(rpcName, res);
|
|
33
36
|
}
|
|
37
|
+
async converse(options) {
|
|
38
|
+
const { aiAgentRunner } = getSingletonServices();
|
|
39
|
+
if (!aiAgentRunner) {
|
|
40
|
+
throw new AIProviderNotConfiguredError();
|
|
41
|
+
}
|
|
42
|
+
const model = options.model ?? this.config.model;
|
|
43
|
+
if (!model) {
|
|
44
|
+
throw new Error(`[user-flow] actor '${this.name}' converse needs a model — pass options.model or set 'model' on the actors service`);
|
|
45
|
+
}
|
|
46
|
+
const threadId = globalThis.crypto.randomUUID();
|
|
47
|
+
const resourceId = `actor:${this.name}`;
|
|
48
|
+
return runConversation({
|
|
49
|
+
persona: this.actorConfig,
|
|
50
|
+
personaName: this.actorConfig.name ?? this.name,
|
|
51
|
+
agentName: options.agent,
|
|
52
|
+
task: options.task,
|
|
53
|
+
evaluate: options.evaluate,
|
|
54
|
+
approvals: options.approvals,
|
|
55
|
+
model,
|
|
56
|
+
maxTurns: options.maxTurns,
|
|
57
|
+
llm: (params) => aiAgentRunner.run(params),
|
|
58
|
+
target: {
|
|
59
|
+
run: (message) => this.agentRun(options.agent, message, threadId, resourceId),
|
|
60
|
+
approve: (runId, decisions) => this.agentApprove(options.agent, runId, decisions),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Start/continue the target agent's run over HTTP as this actor. */
|
|
65
|
+
async agentRun(agentName, message, threadId, resourceId) {
|
|
66
|
+
const raw = await this.postAgent(`agent/${agentName}`, {
|
|
67
|
+
message,
|
|
68
|
+
threadId,
|
|
69
|
+
resourceId,
|
|
70
|
+
});
|
|
71
|
+
return normalizeAgentReply(raw);
|
|
72
|
+
}
|
|
73
|
+
/** Answer the target agent's pending approvals over HTTP and continue. */
|
|
74
|
+
async agentApprove(agentName, runId, decisions) {
|
|
75
|
+
const raw = await this.postAgent(`agent/${agentName}/approve`, {
|
|
76
|
+
runId,
|
|
77
|
+
approvals: decisions,
|
|
78
|
+
});
|
|
79
|
+
return normalizeAgentReply(raw);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* POST an agent HTTP route (raw body, not RPC-wrapped). Signs in lazily: the
|
|
83
|
+
* first call goes out with whatever cookie we hold (none, for a no-auth
|
|
84
|
+
* agent), and only a 401 triggers `login()` + one retry. This lets an actor
|
|
85
|
+
* converse with a no-auth agent without any sign-in wiring, while still
|
|
86
|
+
* authenticating against agents that require a session.
|
|
87
|
+
*/
|
|
88
|
+
async postAgent(subPath, body) {
|
|
89
|
+
const rpcPath = this.config.rpcPath ?? '/rpc';
|
|
90
|
+
const url = `${this.config.apiUrl}${rpcPath}/${subPath}`;
|
|
91
|
+
const send = (cookie) => fetch(url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'content-type': 'application/json',
|
|
95
|
+
origin: this.origin,
|
|
96
|
+
...(cookie ? { cookie } : {}),
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
});
|
|
100
|
+
let res = await send(this.cookie);
|
|
101
|
+
if (res.status === 401) {
|
|
102
|
+
this.cookie = null;
|
|
103
|
+
res = await send(await this.login());
|
|
104
|
+
}
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const text = (await res.text().catch(() => '')).slice(0, 300);
|
|
107
|
+
throw new Error(`[user-flow] agent call '${subPath}' as '${this.name}' returned ${res.status}: ${text}`);
|
|
108
|
+
}
|
|
109
|
+
if (res.status === 204)
|
|
110
|
+
return undefined;
|
|
111
|
+
const text = await res.text();
|
|
112
|
+
return text ? JSON.parse(text) : undefined;
|
|
113
|
+
}
|
|
34
114
|
async postRpc(rpcName, data, cookie) {
|
|
35
115
|
const rpcPath = this.config.rpcPath ?? '/rpc';
|
|
36
116
|
return fetch(`${this.config.apiUrl}${rpcPath}/${rpcName}`, {
|
|
@@ -80,6 +160,26 @@ export class HttpUserFlowActor {
|
|
|
80
160
|
return cookie;
|
|
81
161
|
}
|
|
82
162
|
}
|
|
163
|
+
/** Normalize an agentRun/agentApprove HTTP response into a TargetAgentReply. */
|
|
164
|
+
function normalizeAgentReply(raw) {
|
|
165
|
+
const r = (raw ?? {});
|
|
166
|
+
const pending = Array.isArray(r.pendingApprovals)
|
|
167
|
+
? r.pendingApprovals.map((p) => ({
|
|
168
|
+
toolCallId: String(p.toolCallId),
|
|
169
|
+
toolName: String(p.toolName),
|
|
170
|
+
args: p.args,
|
|
171
|
+
reason: typeof p.reason === 'string' ? p.reason : undefined,
|
|
172
|
+
}))
|
|
173
|
+
: undefined;
|
|
174
|
+
return {
|
|
175
|
+
text: typeof r.text === 'string' ? r.text : '',
|
|
176
|
+
runId: typeof r.runId === 'string' ? r.runId : '',
|
|
177
|
+
status: r.status === 'completed' || r.status === 'suspended'
|
|
178
|
+
? r.status
|
|
179
|
+
: undefined,
|
|
180
|
+
pendingApprovals: pending,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
83
183
|
/**
|
|
84
184
|
* Build the injected `actors` service from the config registry: actor name →
|
|
85
185
|
* lazy HTTP actor. Wire the result as the `actors` singleton service.
|
package/dist/services/index.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
|
|
18
18
|
export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
|
|
19
19
|
export { LocalGatewayService } from './local-gateway-service.js';
|
|
20
20
|
export type { ContentService, SignContentKeyArgs, SignURLArgs, GetUploadURLArgs, UploadURLResult, BucketKeyArgs, WriteFileArgs, CopyFileArgs, } from './content-service.js';
|
|
21
|
-
export type {
|
|
22
|
-
export {
|
|
21
|
+
export type { ScenarioActor, ScenarioActorConfig, ScenarioActors, } from './scenario-actors-service.js';
|
|
22
|
+
export { HttpScenarioActor, createHttpScenarioActors, type HttpScenarioActorsConfig, } from './http-scenario-actors.js';
|
|
23
23
|
export type { JWTService } from './jwt-service.js';
|
|
24
24
|
export type { EmailService, EmailTemplateReference, SendEmailInput, SendEmailResult, SendHTMLEmailInput, SendTemplateEmailInput, SendTextEmailInput, } from './email-service.js';
|
|
25
25
|
export type { Logger } from './logger.js';
|
|
@@ -45,3 +45,6 @@ export { NoopAuditService, createInvocationAudit, resolveAuditActorFromWire, res
|
|
|
45
45
|
export type { AuditActor, AuditConfig, AuditDurability, AuditEvent, AuditEventBatch, AuditLog, AuditLogWriteInput, AuditOutcome, AuditService, AuditSource, ResolvedAuditConfig, } from './audit-service.js';
|
|
46
46
|
export { InMemorySessionStore } from './in-memory-session-store.js';
|
|
47
47
|
export type { MCPMeta, RPCMetaRecord, ServiceMeta, ServicesMetaRecord, MiddlewareDefinitionMeta, MiddlewareInstanceMeta, GroupMeta, MiddlewareGroupsMeta, PermissionDefinitionMeta, PermissionsGroupsMeta, FunctionsMeta, FunctionMeta, MiddlewareMeta, PermissionMeta, AgentsMeta, AgentMeta, EmailsMeta, EmailTemplateMeta, EmailTemplateLocaleMeta, EmailTemplateAssets, } from './meta-service.js';
|
|
48
|
+
export { V8CoverageService, type CoverageService, type CoverageSnapshot, type LineHits, type ScriptCoverage, type FunctionCoverage, type CoverageRange, } from './v8-coverage-service.js';
|
|
49
|
+
export { IstanbulCoverageService } from './istanbul-coverage-service.js';
|
|
50
|
+
export { StubTracker, createStubProxy, getStubTracker, isTestRun, stub, spy, type StubCall, } from './stub-tracker.js';
|
package/dist/services/index.js
CHANGED
|
@@ -17,7 +17,10 @@ export { InMemoryQueueService } from './in-memory-queue-service.js';
|
|
|
17
17
|
export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
|
18
18
|
export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
|
|
19
19
|
export { LocalGatewayService } from './local-gateway-service.js';
|
|
20
|
-
export {
|
|
20
|
+
export { HttpScenarioActor, createHttpScenarioActors, } from './http-scenario-actors.js';
|
|
21
21
|
export { TypedCredentialService } from './typed-credential-service.js';
|
|
22
22
|
export { NoopAuditService, createInvocationAudit, resolveAuditActorFromWire, resolveAuditConfig, } from './audit-service.js';
|
|
23
23
|
export { InMemorySessionStore } from './in-memory-session-store.js';
|
|
24
|
+
export { V8CoverageService, } from './v8-coverage-service.js';
|
|
25
|
+
export { IstanbulCoverageService } from './istanbul-coverage-service.js';
|
|
26
|
+
export { StubTracker, createStubProxy, getStubTracker, isTestRun, stub, spy, } from './stub-tracker.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CoverageService, CoverageSnapshot } from './v8-coverage-service.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads istanbul-instrumented counters from the `__coverage__` global —
|
|
4
|
+
* the coverage backend for runtimes without V8 precise coverage (e.g. Bun,
|
|
5
|
+
* where source files are instrumented at load time by a Bun loader plugin).
|
|
6
|
+
*/
|
|
7
|
+
export declare class IstanbulCoverageService implements CoverageService {
|
|
8
|
+
start(): Promise<void>;
|
|
9
|
+
takeCoverage(): Promise<CoverageSnapshot>;
|
|
10
|
+
reset(): Promise<void>;
|
|
11
|
+
stop(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const getCoverageGlobal = () => globalThis.__coverage__;
|
|
2
|
+
/**
|
|
3
|
+
* Reads istanbul-instrumented counters from the `__coverage__` global —
|
|
4
|
+
* the coverage backend for runtimes without V8 precise coverage (e.g. Bun,
|
|
5
|
+
* where source files are instrumented at load time by a Bun loader plugin).
|
|
6
|
+
*/
|
|
7
|
+
export class IstanbulCoverageService {
|
|
8
|
+
async start() { }
|
|
9
|
+
async takeCoverage() {
|
|
10
|
+
const coverage = getCoverageGlobal();
|
|
11
|
+
const lineHits = new Map();
|
|
12
|
+
for (const file of Object.values(coverage ?? {})) {
|
|
13
|
+
let hits = lineHits.get(file.path);
|
|
14
|
+
if (!hits) {
|
|
15
|
+
hits = new Map();
|
|
16
|
+
lineHits.set(file.path, hits);
|
|
17
|
+
}
|
|
18
|
+
// istanbul line semantics: a statement's count belongs to its start
|
|
19
|
+
// line only, so an enclosing multi-line statement never masks an
|
|
20
|
+
// unexecuted inner statement (e.g. a throw inside a taken if).
|
|
21
|
+
for (const [id, location] of Object.entries(file.statementMap)) {
|
|
22
|
+
const count = file.s[id] ?? 0;
|
|
23
|
+
const line = location.start.line;
|
|
24
|
+
hits.set(line, Math.max(hits.get(line) ?? 0, count));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { kind: 'line-hits', lineHits };
|
|
28
|
+
}
|
|
29
|
+
async reset() {
|
|
30
|
+
for (const file of Object.values(getCoverageGlobal() ?? {})) {
|
|
31
|
+
for (const id of Object.keys(file.s))
|
|
32
|
+
file.s[id] = 0;
|
|
33
|
+
for (const id of Object.keys(file.f))
|
|
34
|
+
file.f[id] = 0;
|
|
35
|
+
for (const id of Object.keys(file.b)) {
|
|
36
|
+
file.b[id] = file.b[id].map(() => 0);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async stop() { }
|
|
41
|
+
}
|
|
@@ -6,7 +6,7 @@ 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
8
|
import type { WorkflowsMeta } from '../wirings/workflow/workflow.types.js';
|
|
9
|
-
import type {
|
|
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';
|
|
12
12
|
import type { CredentialDefinitionsMeta } from '../wirings/credential/credential.types.js';
|
|
@@ -141,7 +141,7 @@ export interface MetaService {
|
|
|
141
141
|
getGatewayMeta(): Promise<GatewaysMeta>;
|
|
142
142
|
getRpcMeta(): Promise<RPCMetaRecord>;
|
|
143
143
|
getWorkflowMeta(): Promise<WorkflowsMeta>;
|
|
144
|
-
|
|
144
|
+
getScenarioActorsMeta(): Promise<Record<string, ScenarioActorConfig>>;
|
|
145
145
|
getTriggerMeta(): Promise<TriggerMeta>;
|
|
146
146
|
getTriggerSourceMeta(): Promise<TriggerSourceMeta>;
|
|
147
147
|
getFunctionsMeta(): Promise<FunctionsMeta>;
|
|
@@ -173,7 +173,7 @@ export declare class LocalMetaService implements MetaService {
|
|
|
173
173
|
private gatewayMetaCache;
|
|
174
174
|
private rpcMetaCache;
|
|
175
175
|
private workflowMetaCache;
|
|
176
|
-
private
|
|
176
|
+
private scenarioActorsMetaCache;
|
|
177
177
|
private triggerMetaCache;
|
|
178
178
|
private triggerSourceMetaCache;
|
|
179
179
|
private functionsMetaCache;
|
|
@@ -200,7 +200,7 @@ export declare class LocalMetaService implements MetaService {
|
|
|
200
200
|
getGatewayMeta(): Promise<GatewaysMeta>;
|
|
201
201
|
getRpcMeta(): Promise<RPCMetaRecord>;
|
|
202
202
|
getWorkflowMeta(): Promise<WorkflowsMeta>;
|
|
203
|
-
|
|
203
|
+
getScenarioActorsMeta(): Promise<Record<string, ScenarioActorConfig>>;
|
|
204
204
|
getTriggerMeta(): Promise<TriggerMeta>;
|
|
205
205
|
getTriggerSourceMeta(): Promise<TriggerSourceMeta>;
|
|
206
206
|
getFunctionsMeta(): Promise<FunctionsMeta>;
|
|
@@ -15,7 +15,7 @@ export class LocalMetaService {
|
|
|
15
15
|
gatewayMetaCache = null;
|
|
16
16
|
rpcMetaCache = null;
|
|
17
17
|
workflowMetaCache = null;
|
|
18
|
-
|
|
18
|
+
scenarioActorsMetaCache = null;
|
|
19
19
|
triggerMetaCache = null;
|
|
20
20
|
triggerSourceMetaCache = null;
|
|
21
21
|
functionsMetaCache = null;
|
|
@@ -64,7 +64,7 @@ export class LocalMetaService {
|
|
|
64
64
|
this.gatewayMetaCache = null;
|
|
65
65
|
this.rpcMetaCache = null;
|
|
66
66
|
this.workflowMetaCache = null;
|
|
67
|
-
this.
|
|
67
|
+
this.scenarioActorsMetaCache = null;
|
|
68
68
|
this.triggerMetaCache = null;
|
|
69
69
|
this.triggerSourceMetaCache = null;
|
|
70
70
|
this.functionsMetaCache = null;
|
|
@@ -203,12 +203,12 @@ export class LocalMetaService {
|
|
|
203
203
|
return this.workflowMetaCache;
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
-
async
|
|
207
|
-
if (this.
|
|
208
|
-
return this.
|
|
209
|
-
const content = await this.readFile('workflow/
|
|
210
|
-
this.
|
|
211
|
-
return this.
|
|
206
|
+
async getScenarioActorsMeta() {
|
|
207
|
+
if (this.scenarioActorsMetaCache)
|
|
208
|
+
return this.scenarioActorsMetaCache;
|
|
209
|
+
const content = await this.readFile('workflow/scenario-actors.gen.json');
|
|
210
|
+
this.scenarioActorsMetaCache = content ? JSON.parse(content) : {};
|
|
211
|
+
return this.scenarioActorsMetaCache;
|
|
212
212
|
}
|
|
213
213
|
async getTriggerMeta() {
|
|
214
214
|
if (this.triggerMetaCache)
|