@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
@@ -0,0 +1,21 @@
1
+ import type { ConverseOptions, ActorFlowVerdict } from '../wirings/actor-flow/actor-flow.types.js';
2
+ /** A synthetic user (a user row flagged `actor`) that workflow steps run as over the real transport */
3
+ export interface ScenarioActor<TAgentName extends string = string> {
4
+ /** Stable actor name (the key in pikku.config.json's actor registry). */
5
+ readonly name: string;
6
+ /** The actor's user email — flows use it for invites/lookups. */
7
+ readonly email: string;
8
+ /** Invoke an exposed RPC as this actor over the real transport. */
9
+ invoke(rpcName: string, data: unknown): Promise<unknown>;
10
+ /** Converse with a Pikku AI agent in this actor's persona and return its verdict */
11
+ converse(options: ConverseOptions<TAgentName>): Promise<ActorFlowVerdict>;
12
+ }
13
+ /** Display/config metadata for an actor (from pikku.config.json) */
14
+ export interface ScenarioActorConfig {
15
+ email: string;
16
+ name?: string;
17
+ jobTitle?: string;
18
+ personality?: string;
19
+ }
20
+ /** The injected `actors` service: actor name → actor. */
21
+ export type ScenarioActors = Record<string, ScenarioActor>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ export type StubCall = {
2
+ service: string;
3
+ method: string;
4
+ args: unknown[];
5
+ };
6
+ /**
7
+ * Tracks calls made to stubbed services and enforces strict mode:
8
+ * once a scenario asserts any call on a service, every recorded call on that
9
+ * service must be verified by end of scenario — otherwise the After hook fails.
10
+ * Services never touched by an assertion stay lenient.
11
+ */
12
+ export declare class StubTracker {
13
+ private readonly calls;
14
+ private readonly touched;
15
+ record(service: string, method: string, args: unknown[]): void;
16
+ getCalls(service?: string): StubCall[];
17
+ reset(): void;
18
+ stub<T>(service: string): T;
19
+ assert(service: string, method: string): void;
20
+ assertCall(service: string, method: string, predicate: (args: unknown[]) => boolean, description: string): void;
21
+ assertNoCalls(service: string, method?: string, predicate?: (args: unknown[]) => boolean, description?: string): void;
22
+ verify(): void;
23
+ }
24
+ /**
25
+ * Creates a Proxy suitable for passing as `existingServices` to
26
+ * `createSingletonServices`. Every property returns `tracker.stub(prop)`
27
+ * EXCEPT `schema`, which returns `undefined` so the service factory creates
28
+ * a real schema service. Stubbing the schema makes validation a no-op —
29
+ * required fields pass silently and tests validate nothing.
30
+ */
31
+ export declare function createStubProxy(tracker: StubTracker): Record<string, unknown>;
32
+ /** The process-wide tracker that `stub()`/`spy()` record into — read by the console's getStubCalls/resetStubs RPCs */
33
+ export declare const getStubTracker: () => StubTracker;
34
+ /** True when the server was started by `pikku dev --test` (sets PIKKU_TEST_RUN) */
35
+ export declare const isTestRun: () => boolean;
36
+ /**
37
+ * Creates a recording fake for a service. Methods present on `impl` run and
38
+ * their result is returned; missing methods resolve `undefined`. Every call
39
+ * is recorded so scenarios can assert via workflow.expectService().
40
+ */
41
+ export declare function stub<T = any>(service: string, impl?: Partial<T>): T;
42
+ /** Wraps a real service so every method call is recorded and passed through */
43
+ export declare function spy<T extends object>(service: string, real: T): T;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Tracks calls made to stubbed services and enforces strict mode:
3
+ * once a scenario asserts any call on a service, every recorded call on that
4
+ * service must be verified by end of scenario — otherwise the After hook fails.
5
+ * Services never touched by an assertion stay lenient.
6
+ */
7
+ export class StubTracker {
8
+ calls = new Map();
9
+ touched = new Set();
10
+ record(service, method, args) {
11
+ const list = this.calls.get(service) ?? [];
12
+ list.push({ method, args, verified: false });
13
+ this.calls.set(service, list);
14
+ }
15
+ getCalls(service) {
16
+ const result = [];
17
+ for (const [name, list] of this.calls) {
18
+ if (service && name !== service)
19
+ continue;
20
+ for (const call of list) {
21
+ result.push({ service: name, method: call.method, args: call.args });
22
+ }
23
+ }
24
+ return result;
25
+ }
26
+ reset() {
27
+ this.calls.clear();
28
+ this.touched.clear();
29
+ }
30
+ stub(service) {
31
+ const self = this;
32
+ return new Proxy(Object.create(null), {
33
+ get(_, method) {
34
+ return (...args) => {
35
+ self.record(service, method, args);
36
+ return Promise.resolve();
37
+ };
38
+ },
39
+ });
40
+ }
41
+ assert(service, method) {
42
+ this.touched.add(service);
43
+ const list = this.calls.get(service) ?? [];
44
+ const idx = list.findIndex((c) => c.method === method && !c.verified);
45
+ if (idx === -1) {
46
+ const seen = list.map((c) => c.method).join(', ') || '(none)';
47
+ throw new Error(`Expected "${service}.${method}" to have been called. Recorded: ${seen}`);
48
+ }
49
+ list[idx].verified = true;
50
+ }
51
+ assertCall(service, method, predicate, description) {
52
+ this.touched.add(service);
53
+ const list = this.calls.get(service) ?? [];
54
+ const idx = list.findIndex((c) => c.method === method && !c.verified && predicate(c.args));
55
+ if (idx === -1) {
56
+ const seen = list
57
+ .filter((c) => c.method === method)
58
+ .map((c) => JSON.stringify(c.args[0]))
59
+ .join('\n ') || '(none)';
60
+ throw new Error(`Expected ${description} but found:\n ${seen}`);
61
+ }
62
+ list[idx].verified = true;
63
+ }
64
+ assertNoCalls(service, method, predicate, description) {
65
+ this.touched.add(service);
66
+ const list = this.calls.get(service) ?? [];
67
+ const relevant = (method ? list.filter((c) => c.method === method) : list).filter((c) => !predicate || predicate(c.args));
68
+ if (relevant.length > 0) {
69
+ const calls = relevant
70
+ .map((c) => `${c.method}(${c.args.map((a) => JSON.stringify(a)).join(', ')})`)
71
+ .join('\n ');
72
+ const what = description ?? `"${service}${method ? '.' + method : ''}"`;
73
+ throw new Error(`Expected no ${what} calls but got:\n ${calls}`);
74
+ }
75
+ }
76
+ verify() {
77
+ const errors = [];
78
+ for (const service of this.touched) {
79
+ const unverified = (this.calls.get(service) ?? []).filter((c) => !c.verified);
80
+ for (const c of unverified) {
81
+ const argStr = c.args.map((a) => JSON.stringify(a)).join(', ');
82
+ errors.push(` ${service}.${c.method}(${argStr})`);
83
+ }
84
+ }
85
+ if (errors.length) {
86
+ throw new Error(`Unexpected stub calls — assert them in the scenario or remove the side effect:\n${errors.join('\n')}`);
87
+ }
88
+ }
89
+ }
90
+ /**
91
+ * Creates a Proxy suitable for passing as `existingServices` to
92
+ * `createSingletonServices`. Every property returns `tracker.stub(prop)`
93
+ * EXCEPT `schema`, which returns `undefined` so the service factory creates
94
+ * a real schema service. Stubbing the schema makes validation a no-op —
95
+ * required fields pass silently and tests validate nothing.
96
+ */
97
+ export function createStubProxy(tracker) {
98
+ return new Proxy({}, {
99
+ get(_, prop) {
100
+ if (prop === 'schema')
101
+ return undefined;
102
+ return tracker.stub(prop);
103
+ },
104
+ });
105
+ }
106
+ const defaultStubTracker = new StubTracker();
107
+ /** The process-wide tracker that `stub()`/`spy()` record into — read by the console's getStubCalls/resetStubs RPCs */
108
+ export const getStubTracker = () => defaultStubTracker;
109
+ /** True when the server was started by `pikku dev --test` (sets PIKKU_TEST_RUN) */
110
+ export const isTestRun = () => globalThis
111
+ .process?.env?.PIKKU_TEST_RUN === 'true';
112
+ /**
113
+ * Creates a recording fake for a service. Methods present on `impl` run and
114
+ * their result is returned; missing methods resolve `undefined`. Every call
115
+ * is recorded so scenarios can assert via workflow.expectService().
116
+ */
117
+ export function stub(service, impl) {
118
+ return new Proxy((impl ?? {}), {
119
+ get(target, method) {
120
+ if (typeof method === 'symbol')
121
+ return target[method];
122
+ const real = target[method];
123
+ if (typeof real !== 'function' && real !== undefined)
124
+ return real;
125
+ return (...args) => {
126
+ defaultStubTracker.record(service, method, args);
127
+ return real ? real.apply(target, args) : Promise.resolve(undefined);
128
+ };
129
+ },
130
+ });
131
+ }
132
+ /** Wraps a real service so every method call is recorded and passed through */
133
+ export function spy(service, real) {
134
+ return new Proxy(real, {
135
+ get(target, method) {
136
+ const value = target[method];
137
+ if (typeof method === 'symbol' || typeof value !== 'function') {
138
+ return value;
139
+ }
140
+ return (...args) => {
141
+ defaultStubTracker.record(service, method, args);
142
+ return value.apply(target, args);
143
+ };
144
+ },
145
+ });
146
+ }
@@ -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
@@ -0,0 +1,41 @@
1
+ export interface CoverageRange {
2
+ startOffset: number;
3
+ endOffset: number;
4
+ count: number;
5
+ }
6
+ export interface FunctionCoverage {
7
+ functionName: string;
8
+ isBlockCoverage: boolean;
9
+ ranges: CoverageRange[];
10
+ }
11
+ export interface ScriptCoverage {
12
+ scriptId: string;
13
+ url: string;
14
+ functions: FunctionCoverage[];
15
+ }
16
+ export type LineHits = Map<string, Map<number, number>>;
17
+ export type CoverageSnapshot = {
18
+ kind: 'v8-scripts';
19
+ scripts: ScriptCoverage[];
20
+ getScriptSource: (scriptId: string) => Promise<string>;
21
+ } | {
22
+ kind: 'line-hits';
23
+ lineHits: LineHits;
24
+ };
25
+ export interface CoverageService {
26
+ start(): Promise<void>;
27
+ takeCoverage(): Promise<CoverageSnapshot>;
28
+ reset(): Promise<void>;
29
+ stop(): Promise<void>;
30
+ }
31
+ export declare class V8CoverageService implements CoverageService {
32
+ private session;
33
+ private startPromise;
34
+ private post;
35
+ start(): Promise<void>;
36
+ private doStart;
37
+ takeCoverage(): Promise<CoverageSnapshot>;
38
+ getScriptSource(scriptId: string): Promise<string>;
39
+ reset(): Promise<void>;
40
+ stop(): Promise<void>;
41
+ }
@@ -0,0 +1,63 @@
1
+ // node:inspector is imported lazily inside start() so this module stays
2
+ // loadable on runtimes without it (e.g. Cloudflare Workers).
3
+ export class V8CoverageService {
4
+ session = null;
5
+ startPromise = null;
6
+ post(method, params) {
7
+ const session = this.session;
8
+ if (!session) {
9
+ throw new Error('V8CoverageService not started — call start() first');
10
+ }
11
+ return new Promise((resolve, reject) => {
12
+ session.post(method, params, (err, result) => err ? reject(err) : resolve(result));
13
+ });
14
+ }
15
+ start() {
16
+ this.startPromise ??= this.doStart();
17
+ return this.startPromise;
18
+ }
19
+ async doStart() {
20
+ const inspector = await import('node:inspector');
21
+ this.session = new inspector.Session();
22
+ this.session.connect();
23
+ await this.post('Profiler.enable');
24
+ await this.post('Debugger.enable');
25
+ await this.post('Profiler.startPreciseCoverage', {
26
+ callCount: true,
27
+ detailed: true,
28
+ });
29
+ }
30
+ async takeCoverage() {
31
+ const { result } = await this.post('Profiler.takePreciseCoverage');
32
+ return {
33
+ kind: 'v8-scripts',
34
+ scripts: result.filter((script) => script.url.startsWith('file://')),
35
+ getScriptSource: (scriptId) => this.getScriptSource(scriptId),
36
+ };
37
+ }
38
+ async getScriptSource(scriptId) {
39
+ const { scriptSource } = await this.post('Debugger.getScriptSource', { scriptId });
40
+ return scriptSource;
41
+ }
42
+ async reset() {
43
+ await this.post('Profiler.stopPreciseCoverage');
44
+ await this.post('Profiler.startPreciseCoverage', {
45
+ callCount: true,
46
+ detailed: true,
47
+ });
48
+ }
49
+ async stop() {
50
+ if (!this.session)
51
+ return;
52
+ try {
53
+ await this.post('Profiler.stopPreciseCoverage');
54
+ await this.post('Profiler.disable');
55
+ await this.post('Debugger.disable');
56
+ }
57
+ finally {
58
+ this.session.disconnect();
59
+ this.session = null;
60
+ this.startPromise = null;
61
+ }
62
+ }
63
+ }
@@ -19,7 +19,7 @@ import type { SchedulerService } from '../services/scheduler-service.js';
19
19
  import type { DeploymentService } from '../services/deployment-service.js';
20
20
  import type { AIStorageService } from '../services/ai-storage-service.js';
21
21
  import type { ContentService } from '../services/content-service.js';
22
- import type { UserFlowActors } from '../services/user-flow-actors-service.js';
22
+ import type { ScenarioActors } from '../services/scenario-actors-service.js';
23
23
  import type { AIAgentRunnerService } from '../services/ai-agent-runner-service.js';
24
24
  import type { AIRunStateService } from '../services/ai-run-state-service.js';
25
25
  import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js';
@@ -28,6 +28,7 @@ import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js';
28
28
  import type { CredentialService } from '../services/credential-service.js';
29
29
  import type { EmailService } from '../services/email-service.js';
30
30
  import type { MetaService } from '../services/meta-service.js';
31
+ import type { CoverageService } from '../services/v8-coverage-service.js';
31
32
  import type { SessionStore } from '../services/session-store.js';
32
33
  import type { AuditDurability, AuditLog, AuditService } from '../services/audit-service.js';
33
34
  export type PikkuWiringTypes = 'http' | 'scheduler' | 'trigger' | 'channel' | 'rpc' | 'queue' | 'mcp' | 'cli' | 'workflow' | 'agent' | 'gateway';
@@ -110,6 +111,12 @@ export type FunctionMeta = FunctionRuntimeMeta & Partial<{
110
111
  isDirectFunction: boolean;
111
112
  sourceFile: string;
112
113
  exportedName: string;
114
+ /** File containing the handler body when it differs from sourceFile (imported handlers) */
115
+ bodySourceFile?: string;
116
+ /** 1-indexed first line of the handler body (verbose meta; coverage mapping) */
117
+ bodyStart: number;
118
+ /** 1-indexed last line of the handler body (verbose meta; coverage mapping) */
119
+ bodyEnd: number;
113
120
  } & CommonWireMeta>;
114
121
  export type FunctionsRuntimeMeta = Record<string, FunctionRuntimeMeta>;
115
122
  export type FunctionsMeta = Record<string, FunctionMeta>;
@@ -186,7 +193,7 @@ export type CoreConfig<Config extends Record<string, unknown> = {}> = {
186
193
  export interface CoreUserSession {
187
194
  userId?: string;
188
195
  orgId?: string;
189
- /** True when the session belongs to a synthetic user-flow actor — lets audits/analytics address synthetic traffic */
196
+ /** True when the session belongs to a synthetic scenario actor — lets audits/analytics address synthetic traffic */
190
197
  actor?: boolean;
191
198
  }
192
199
  /**
@@ -233,6 +240,8 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
233
240
  emailService?: EmailService;
234
241
  /** Meta service for reading .pikku metadata files (filesystem on Node, R2/KV on CF) */
235
242
  metaService?: MetaService;
243
+ /** V8 precise-coverage collector (`pikku dev --coverage` only) */
244
+ coverageService?: CoverageService;
236
245
  /** Audit service for durable or staged audit event capture */
237
246
  audit?: AuditService;
238
247
  /**
@@ -266,8 +275,8 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
266
275
  queue: PikkuQueue;
267
276
  cli: PikkuCLI;
268
277
  workflow: TypedWorkflow;
269
- /** User-flow actor registry (user-flow runs only) — pass into workflow.do as `{ actor: actors.x }` */
270
- actors: UserFlowActors;
278
+ /** Scenario actor registry (scenario runs only) — pass into workflow.do as `{ actor: actors.x }` */
279
+ actors: ScenarioActors;
271
280
  workflowStep: WorkflowStepWire;
272
281
  graph: PikkuGraphWire;
273
282
  trigger: PikkuTrigger<TriggerOutput>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * How an actor agent answers the target agent's tool-approval requests during
3
+ * a conversation.
4
+ * - `'in-persona'` — the actor agent decides as the persona would (default).
5
+ * - `'always'` — approve every request (stress the happy path).
6
+ * - `'never'` — deny every request (exercise refusal handling).
7
+ */
8
+ export type ActorFlowApprovalPolicy = 'in-persona' | 'always' | 'never';
9
+ /**
10
+ * Options for `actor.converse(...)` — a dynamic conversation an actor holds
11
+ * with a target Pikku AI agent, in the actor's own persona. `TAgentName` is
12
+ * bound to the generated union of agent names in a typed project.
13
+ */
14
+ export interface ConverseOptions<TAgentName extends string = string> {
15
+ /** Target Pikku AI agent name to converse with. */
16
+ agent: TAgentName;
17
+ /** What the actor is trying to get the agent to accomplish. */
18
+ task: string;
19
+ /** Natural-language success criterion the actor evaluates at the end. */
20
+ evaluate: string;
21
+ /** How the actor answers the agent's tool-approval requests. Default `'in-persona'`. */
22
+ approvals?: ActorFlowApprovalPolicy;
23
+ /** Model the persona uses for its own turns/decisions. Falls back to the actor service default. */
24
+ model?: string;
25
+ /** Hard cap on conversation turns before forcing evaluation. Default 12. */
26
+ maxTurns?: number;
27
+ }
28
+ /**
29
+ * The verdict a conversation produces: the persona's LLM self-evaluation of
30
+ * whether the task was met. Deterministic checks are the caller's job — they
31
+ * already hold the actor and can `actor.invoke(...)` afterwards.
32
+ */
33
+ export interface ActorFlowVerdict {
34
+ /** Whether the actor judged the task accomplished. */
35
+ passed: boolean;
36
+ /** The actor's reasoning for its verdict. */
37
+ reasoning: string;
38
+ /** The conversation transcript, for debugging/reporting. */
39
+ transcript: string[];
40
+ }
41
+ /** A pending tool-approval request surfaced by the target agent. */
42
+ export interface TargetPendingApproval {
43
+ toolCallId: string;
44
+ toolName: string;
45
+ args: unknown;
46
+ reason?: string;
47
+ }
48
+ /** A normalized reply from the target agent, independent of transport. */
49
+ export interface TargetAgentReply {
50
+ text: string;
51
+ runId: string;
52
+ status?: 'completed' | 'suspended';
53
+ pendingApprovals?: TargetPendingApproval[];
54
+ }
55
+ /**
56
+ * Drives the target agent. In production this is HTTP-backed (the actor's
57
+ * `agentRun` / `agentApprove` calls as the signed-in actor); the conversation
58
+ * engine only sees this transport-agnostic contract.
59
+ */
60
+ export interface TargetAgentDriver {
61
+ /** Send a message, starting or continuing the target agent's run. */
62
+ run(message: string): Promise<TargetAgentReply>;
63
+ /** Answer the target agent's pending approvals and continue its run. */
64
+ approve(runId: string, decisions: {
65
+ toolCallId: string;
66
+ approved: boolean;
67
+ }[]): Promise<TargetAgentReply>;
68
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Actor flow module exports.
3
+ *
4
+ * An actor holds a dynamic conversation with a target Pikku AI agent via
5
+ * `actor.converse(...)` — playing its own persona, approving the agent's tool
6
+ * requests in-persona, and evaluating whether the task was accomplished. The
7
+ * conversation engine here is transport-agnostic; the actor drives the target
8
+ * over HTTP.
9
+ */
10
+ export type { ActorFlowApprovalPolicy, ActorFlowVerdict, ConverseOptions, TargetAgentReply, TargetPendingApproval, TargetAgentDriver, } from './actor-flow.types.js';
11
+ export { runConversation, type RunConversationParams, type PersonaLLM, } from './run-conversation.js';
@@ -0,0 +1 @@
1
+ export { runConversation, } from './run-conversation.js';
@@ -0,0 +1,36 @@
1
+ import type { ActorFlowApprovalPolicy, ActorFlowVerdict, TargetAgentDriver } from './actor-flow.types.js';
2
+ import type { ScenarioActorConfig } from '../../services/scenario-actors-service.js';
3
+ import type { AIAgentRunnerParams, AIAgentStepResult } from '../../services/ai-agent-runner-service.js';
4
+ /** The LLM call the persona uses for its own turns/decisions/evaluation. */
5
+ export type PersonaLLM = (params: AIAgentRunnerParams) => Promise<AIAgentStepResult>;
6
+ export interface RunConversationParams {
7
+ /** Persona config (personality/jobTitle/name) that shapes how the actor talks. */
8
+ persona: ScenarioActorConfig;
9
+ /** Stable persona name (for transcript labelling). */
10
+ personaName: string;
11
+ /** What the actor is trying to get the target agent to accomplish. */
12
+ task: string;
13
+ /** Natural-language success criterion the actor evaluates at the end. */
14
+ evaluate: string;
15
+ /** How the actor answers the target agent's tool-approval requests. */
16
+ approvals?: ActorFlowApprovalPolicy;
17
+ /** Model the persona uses for its own turns/decisions. */
18
+ model: string;
19
+ /** Hard cap on conversation turns. Default 12. */
20
+ maxTurns?: number;
21
+ /** Hard cap on tool-approval rounds within a single target turn. Default 16. */
22
+ maxApprovalRounds?: number;
23
+ /** Transport that drives the target agent (HTTP in production). */
24
+ target: TargetAgentDriver;
25
+ /** The persona's own LLM. */
26
+ llm: PersonaLLM;
27
+ /** Display name of the target agent (transcript labelling). */
28
+ agentName: string;
29
+ }
30
+ /**
31
+ * Run a conversation: an LLM-driven persona holds a real multi-turn exchange
32
+ * with a target agent (driven via the injected transport), answers the target's
33
+ * tool-approval requests in-persona, then evaluates whether the task was met.
34
+ * Deterministic checks are the caller's responsibility.
35
+ */
36
+ export declare function runConversation(params: RunConversationParams): Promise<ActorFlowVerdict>;