@pikku/core 0.12.51 → 0.12.52

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 (41) hide show
  1. package/CHANGELOG.md +59 -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 +2 -2
  7. package/dist/services/index.js +1 -1
  8. package/dist/services/meta-service.d.ts +4 -4
  9. package/dist/services/meta-service.js +8 -8
  10. package/dist/services/scenario-actors-service.d.ts +39 -0
  11. package/dist/services/scenario-actors-service.js +1 -0
  12. package/dist/services/user-flow-actors-service.d.ts +11 -1
  13. package/dist/types/core.types.d.ts +4 -4
  14. package/dist/wirings/actor-flow/actor-flow.types.d.ts +68 -0
  15. package/dist/wirings/actor-flow/actor-flow.types.js +1 -0
  16. package/dist/wirings/actor-flow/index.d.ts +11 -0
  17. package/dist/wirings/actor-flow/index.js +1 -0
  18. package/dist/wirings/actor-flow/run-conversation.d.ts +36 -0
  19. package/dist/wirings/actor-flow/run-conversation.js +181 -0
  20. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +6 -6
  21. package/dist/wirings/workflow/pikku-workflow-service.d.ts +10 -2
  22. package/dist/wirings/workflow/pikku-workflow-service.js +42 -2
  23. package/dist/wirings/workflow/workflow.types.d.ts +6 -6
  24. package/package.json +2 -1
  25. package/src/services/http-scenario-actors-converse.test.ts +222 -0
  26. package/src/services/{http-user-flow-actors.test.ts → http-scenario-actors.test.ts} +17 -7
  27. package/src/services/http-scenario-actors.ts +269 -0
  28. package/src/services/index.ts +8 -8
  29. package/src/services/meta-service.ts +9 -9
  30. package/src/services/{user-flow-actors-service.ts → scenario-actors-service.ts} +18 -4
  31. package/src/types/core.types.ts +4 -4
  32. package/src/wirings/actor-flow/actor-flow.types.ts +73 -0
  33. package/src/wirings/actor-flow/index.ts +22 -0
  34. package/src/wirings/actor-flow/run-conversation.test.ts +176 -0
  35. package/src/wirings/actor-flow/run-conversation.ts +285 -0
  36. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +6 -6
  37. package/src/wirings/workflow/pikku-workflow-service.ts +50 -5
  38. package/src/wirings/workflow/{user-flow-step.test.ts → scenario-step.test.ts} +19 -14
  39. package/src/wirings/workflow/workflow.types.ts +6 -6
  40. package/tsconfig.tsbuildinfo +1 -1
  41. package/src/services/http-user-flow-actors.ts +0 -132
package/CHANGELOG.md CHANGED
@@ -1,3 +1,62 @@
1
+ ## 0.12.52
2
+
3
+ ### Patch Changes
4
+
5
+ - 61c9ce9: Add `actor.converse(...)` — actor agents for user journeys (#850)
6
+
7
+ An actor can now hold a dynamic, LLM-driven conversation with a target Pikku AI
8
+ agent in its own persona:
9
+
10
+ ```ts
11
+ const verdict = await actors.pm.converse({
12
+ agent: 'todoBot',
13
+ task: 'Get a todo created for the launch',
14
+ evaluate: 'A todo about the launch now exists',
15
+ })
16
+ // verdict: { passed, reasoning, transcript }
17
+ // then assert deterministically as the same actor:
18
+ const todos = await actors.pm.invoke('listTodos', {})
19
+ ```
20
+
21
+ The actor drives the target over the real transport (the agent's own
22
+ `agentRun` / `agentApprove` HTTP routes, signed in as the actor), plays the
23
+ persona from its `pikku.config.json` config, answers the agent's tool-approval
24
+ requests in-persona (`approvals: 'in-persona' | 'always' | 'never'`), and
25
+ returns its verdict on whether the task was met. Deterministic checks stay the
26
+ caller's job — they already hold the actor.
27
+
28
+ The conversation engine is transport-agnostic (persona LLM + injected target
29
+ driver); the persona's own turns run in-process via the configured
30
+ `aiAgentRunner` (`model` from the call or the actors-service default).
31
+
32
+ `agent` is typed against the generated agent-name union (`keyof AgentMap`), so
33
+ it's author-time checked and autocompleted in a typed project.
34
+
35
+ - f1f39f8: Bound the actor-flow approval loop (#850)
36
+
37
+ `converseWithTarget` now caps suspend→approve rounds within a single target turn
38
+ (default 16, override via `maxApprovalRounds`). A cooperative target completes
39
+ after a handful of rounds; a buggy or uncooperative one — e.g. re-requesting a
40
+ tool the persona keeps denying — previously could spin the inner loop forever
41
+ without ever spending a `maxTurns` credit. Exceeding the cap now throws instead
42
+ of hanging.
43
+
44
+ - c45e98d: Run user flows from the console, actors and all (#850)
45
+
46
+ Starting a `user-flow` workflow without explicit run actors (as the console's
47
+ Run button does) now auto-builds HTTP actors from `USER_FLOW_ACTOR_SECRET` and
48
+ `API_URL`: each actor signs in via the actor auth plugin — which mints the
49
+ `actor: true` user row on first sign-in — and drives its steps over HTTP as
50
+ that persona. When the secret or API base URL isn't configured the run simply
51
+ proceeds without actors (with a warning) instead of failing.
52
+
53
+ The workflow-detail view also gains the shared console header: the workflow
54
+ selector and the "complex workflow" note now live in the header bar, the right
55
+ details panel hides when it has nothing to show, and step nodes display their
56
+ DSL labels (e.g. `Double ${item}`).
57
+
58
+ - 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.
59
+
1
60
  ## 0.12.51
2
61
 
3
62
  ### 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';
@@ -17,7 +17,7 @@ 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';
@@ -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)
@@ -0,0 +1,39 @@
1
+ import type { ConverseOptions, ActorFlowVerdict } from '../wirings/actor-flow/actor-flow.types.js';
2
+ /**
3
+ * A scenario actor: a synthetic user (a normal user row flagged `actor`) that
4
+ * workflow steps can run as. Passed to `workflow.do(step, rpc, data, { actor })`
5
+ * — the step then goes through the actor's authenticated client over the REAL
6
+ * transport (auth middleware, permissions, serialization all exercised),
7
+ * never through internal dispatch. Login is lazy: the first `invoke` signs the
8
+ * actor in and the session is cached for the actor's lifetime.
9
+ */
10
+ export interface ScenarioActor<TAgentName extends string = string> {
11
+ /** Stable actor name (the key in pikku.config.json's actor registry). */
12
+ readonly name: string;
13
+ /** The actor's user email — flows use it for invites/lookups. */
14
+ readonly email: string;
15
+ /** Invoke an exposed RPC as this actor over the real transport. */
16
+ invoke(rpcName: string, data: unknown): Promise<unknown>;
17
+ /**
18
+ * Hold a dynamic conversation with a target Pikku AI agent, in THIS actor's
19
+ * persona (personality/jobTitle). Drives the target over the real transport
20
+ * as the signed-in actor, answers its tool-approval requests in-persona, and
21
+ * returns the actor's verdict on whether the task was met. Deterministic
22
+ * checks are the caller's job — use `invoke` afterwards. In a typed project
23
+ * `agent` is constrained to the generated union of agent names.
24
+ */
25
+ converse(options: ConverseOptions<TAgentName>): Promise<ActorFlowVerdict>;
26
+ }
27
+ /**
28
+ * Display/config metadata for an actor (from pikku.config.json). The email
29
+ * identifies the actor's user row; personality/jobTitle exist for the console
30
+ * screen and for agent-driven flows (the agent plays the persona).
31
+ */
32
+ export interface ScenarioActorConfig {
33
+ email: string;
34
+ name?: string;
35
+ jobTitle?: string;
36
+ personality?: string;
37
+ }
38
+ /** The injected `actors` service: actor name → actor. */
39
+ export type ScenarioActors = Record<string, ScenarioActor>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
+ import type { ConverseOptions, ActorFlowVerdict } from '../wirings/actor-flow/actor-flow.types.js';
1
2
  /**
2
3
  * A user-flow actor: a synthetic user (a normal user row flagged `actor`) that
3
4
  * workflow steps can run as. Passed to `workflow.do(step, rpc, data, { actor })`
@@ -6,13 +7,22 @@
6
7
  * never through internal dispatch. Login is lazy: the first `invoke` signs the
7
8
  * actor in and the session is cached for the actor's lifetime.
8
9
  */
9
- export interface UserFlowActor {
10
+ export interface UserFlowActor<TAgentName extends string = string> {
10
11
  /** Stable actor name (the key in pikku.config.json's actor registry). */
11
12
  readonly name: string;
12
13
  /** The actor's user email — flows use it for invites/lookups. */
13
14
  readonly email: string;
14
15
  /** Invoke an exposed RPC as this actor over the real transport. */
15
16
  invoke(rpcName: string, data: unknown): Promise<unknown>;
17
+ /**
18
+ * Hold a dynamic conversation with a target Pikku AI agent, in THIS actor's
19
+ * persona (personality/jobTitle). Drives the target over the real transport
20
+ * as the signed-in actor, answers its tool-approval requests in-persona, and
21
+ * returns the actor's verdict on whether the task was met. Deterministic
22
+ * checks are the caller's job — use `invoke` afterwards. In a typed project
23
+ * `agent` is constrained to the generated union of agent names.
24
+ */
25
+ converse(options: ConverseOptions<TAgentName>): Promise<ActorFlowVerdict>;
16
26
  }
17
27
  /**
18
28
  * Display/config metadata for an actor (from pikku.config.json). The email