@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/services/http-scenario-actors.d.ts +67 -0
  3. package/dist/services/http-scenario-actors.js +193 -0
  4. package/dist/services/http-user-flow-actors.d.ts +20 -0
  5. package/dist/services/http-user-flow-actors.js +100 -0
  6. package/dist/services/index.d.ts +5 -2
  7. package/dist/services/index.js +4 -1
  8. package/dist/services/istanbul-coverage-service.d.ts +12 -0
  9. package/dist/services/istanbul-coverage-service.js +41 -0
  10. package/dist/services/meta-service.d.ts +4 -4
  11. package/dist/services/meta-service.js +8 -8
  12. package/dist/services/scenario-actors-service.d.ts +21 -0
  13. package/dist/services/scenario-actors-service.js +1 -0
  14. package/dist/services/stub-tracker.d.ts +43 -0
  15. package/dist/services/stub-tracker.js +146 -0
  16. package/dist/services/user-flow-actors-service.d.ts +11 -1
  17. package/dist/services/v8-coverage-service.d.ts +41 -0
  18. package/dist/services/v8-coverage-service.js +63 -0
  19. package/dist/types/core.types.d.ts +13 -4
  20. package/dist/wirings/actor-flow/actor-flow.types.d.ts +68 -0
  21. package/dist/wirings/actor-flow/actor-flow.types.js +1 -0
  22. package/dist/wirings/actor-flow/index.d.ts +11 -0
  23. package/dist/wirings/actor-flow/index.js +1 -0
  24. package/dist/wirings/actor-flow/run-conversation.d.ts +36 -0
  25. package/dist/wirings/actor-flow/run-conversation.js +181 -0
  26. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +22 -6
  27. package/dist/wirings/workflow/pikku-workflow-service.d.ts +4 -2
  28. package/dist/wirings/workflow/pikku-workflow-service.js +92 -2
  29. package/dist/wirings/workflow/workflow.types.d.ts +7 -7
  30. package/package.json +2 -1
  31. package/src/services/http-scenario-actors-converse.test.ts +222 -0
  32. package/src/services/{http-user-flow-actors.test.ts → http-scenario-actors.test.ts} +17 -7
  33. package/src/services/http-scenario-actors.ts +269 -0
  34. package/src/services/index.ts +27 -8
  35. package/src/services/istanbul-coverage-service.test.ts +66 -0
  36. package/src/services/istanbul-coverage-service.ts +65 -0
  37. package/src/services/meta-service.ts +9 -9
  38. package/src/services/scenario-actors-service.ts +27 -0
  39. package/src/services/stub-tracker.test.ts +104 -0
  40. package/src/services/stub-tracker.ts +185 -0
  41. package/src/services/v8-coverage-service.test.ts +69 -0
  42. package/src/services/v8-coverage-service.ts +121 -0
  43. package/src/types/core.types.ts +13 -4
  44. package/src/wirings/actor-flow/actor-flow.types.ts +73 -0
  45. package/src/wirings/actor-flow/index.ts +22 -0
  46. package/src/wirings/actor-flow/run-conversation.test.ts +176 -0
  47. package/src/wirings/actor-flow/run-conversation.ts +285 -0
  48. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -6
  49. package/src/wirings/workflow/pikku-workflow-service.ts +144 -5
  50. package/src/wirings/workflow/{user-flow-step.test.ts → scenario-step.test.ts} +19 -14
  51. package/src/wirings/workflow/workflow.types.ts +8 -6
  52. package/tsconfig.tsbuildinfo +1 -1
  53. package/src/services/http-user-flow-actors.ts +0 -132
  54. 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.
@@ -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 { UserFlowActor, UserFlowActorConfig, UserFlowActors, } from './user-flow-actors-service.js';
22
- export { HttpUserFlowActor, createHttpUserFlowActors, type HttpUserFlowActorsConfig, } from './http-user-flow-actors.js';
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';
@@ -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 { HttpUserFlowActor, createHttpUserFlowActors, } from './http-user-flow-actors.js';
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 { UserFlowActorConfig } from './user-flow-actors-service.js';
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
- getUserFlowActorsMeta(): Promise<Record<string, UserFlowActorConfig>>;
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 userFlowActorsMetaCache;
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
- getUserFlowActorsMeta(): Promise<Record<string, UserFlowActorConfig>>;
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
- userFlowActorsMetaCache = null;
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.userFlowActorsMetaCache = null;
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 getUserFlowActorsMeta() {
207
- if (this.userFlowActorsMetaCache)
208
- return this.userFlowActorsMetaCache;
209
- const content = await this.readFile('workflow/user-flow-actors.gen.json');
210
- this.userFlowActorsMetaCache = content ? JSON.parse(content) : {};
211
- return this.userFlowActorsMetaCache;
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)