@pikku/core 0.12.69 → 0.12.71

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 (118) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +34 -2
  4. package/dist/function/functions.types.d.ts +27 -0
  5. package/dist/index.d.ts +1 -1
  6. package/dist/internal.d.ts +1 -1
  7. package/dist/internal.js +1 -1
  8. package/dist/pikku-state.js +1 -0
  9. package/dist/services/http-scenario-actors.d.ts +12 -4
  10. package/dist/services/http-scenario-actors.js +47 -45
  11. package/dist/services/in-memory-queue-service.d.ts +6 -0
  12. package/dist/services/in-memory-queue-service.js +8 -1
  13. package/dist/services/in-memory-workflow-service.d.ts +3 -5
  14. package/dist/services/in-memory-workflow-service.js +10 -19
  15. package/dist/services/index.d.ts +2 -1
  16. package/dist/services/index.js +1 -0
  17. package/dist/services/meta-service.d.ts +5 -1
  18. package/dist/services/meta-service.js +44 -18
  19. package/dist/services/scenario-actors-service.d.ts +108 -2
  20. package/dist/services/scenario-actors-service.js +40 -1
  21. package/dist/services/workflow-service.d.ts +7 -5
  22. package/dist/types/core.types.d.ts +28 -3
  23. package/dist/types/state.types.d.ts +3 -1
  24. package/dist/wirings/actor-flow/actor-flow.types.d.ts +1 -1
  25. package/dist/wirings/actor-flow/index.d.ts +1 -1
  26. package/dist/wirings/actor-flow/run-conversation.d.ts +10 -10
  27. package/dist/wirings/actor-flow/run-conversation.js +27 -27
  28. package/dist/wirings/ai-agent/ai-agent-agui.js +0 -8
  29. package/dist/wirings/ai-agent/ai-agent-prepare.js +1 -2
  30. package/dist/wirings/ai-agent/ai-agent.types.d.ts +0 -6
  31. package/dist/wirings/cli/command-parser.js +11 -1
  32. package/dist/wirings/rpc/rpc-runner.js +1 -1
  33. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +52 -3
  34. package/dist/wirings/workflow/feature.d.ts +28 -0
  35. package/dist/wirings/workflow/feature.js +57 -0
  36. package/dist/wirings/workflow/graph/graph-runner.js +3 -2
  37. package/dist/wirings/workflow/graph/graph-validation.d.ts +0 -2
  38. package/dist/wirings/workflow/graph/graph-validation.js +0 -142
  39. package/dist/wirings/workflow/graph/index.d.ts +1 -1
  40. package/dist/wirings/workflow/graph/index.js +1 -1
  41. package/dist/wirings/workflow/index.d.ts +13 -3
  42. package/dist/wirings/workflow/index.js +15 -2
  43. package/dist/wirings/workflow/pikku-scenario-service.d.ts +121 -0
  44. package/dist/wirings/workflow/pikku-scenario-service.js +419 -0
  45. package/dist/wirings/workflow/pikku-workflow-service.d.ts +170 -23
  46. package/dist/wirings/workflow/pikku-workflow-service.js +338 -297
  47. package/dist/wirings/workflow/scenario-cookie-jar.d.ts +29 -0
  48. package/dist/wirings/workflow/scenario-cookie-jar.js +51 -0
  49. package/dist/wirings/workflow/scenario-poll.d.ts +20 -0
  50. package/dist/wirings/workflow/scenario-poll.js +25 -0
  51. package/dist/wirings/workflow/scenario-prose.d.ts +38 -0
  52. package/dist/wirings/workflow/scenario-prose.js +45 -0
  53. package/dist/wirings/workflow/scenario-step-guards.d.ts +16 -0
  54. package/dist/wirings/workflow/scenario-step-guards.js +29 -0
  55. package/dist/wirings/workflow/scenario-step.types.d.ts +148 -0
  56. package/dist/wirings/workflow/scenario-step.types.js +1 -0
  57. package/dist/wirings/workflow/workflow.types.d.ts +82 -8
  58. package/package.json +3 -1
  59. package/src/function/functions.types.ts +32 -0
  60. package/src/index.ts +1 -0
  61. package/src/internal.ts +5 -1
  62. package/src/pikku-state.ts +1 -0
  63. package/src/services/http-scenario-actors.test.ts +85 -1
  64. package/src/services/http-scenario-actors.ts +65 -51
  65. package/src/services/in-memory-queue-service.test.ts +66 -1
  66. package/src/services/in-memory-queue-service.ts +13 -2
  67. package/src/services/in-memory-workflow-service.ts +12 -25
  68. package/src/services/index.ts +5 -0
  69. package/src/services/meta-service.test.ts +79 -0
  70. package/src/services/meta-service.ts +61 -26
  71. package/src/services/scenario-actors-service.ts +157 -2
  72. package/src/services/workflow-service.ts +7 -4
  73. package/src/types/core.types.ts +34 -2
  74. package/src/types/state.types.ts +3 -0
  75. package/src/wirings/actor-flow/actor-flow.types.ts +1 -1
  76. package/src/wirings/actor-flow/index.ts +1 -1
  77. package/src/wirings/actor-flow/run-conversation.test.ts +12 -6
  78. package/src/wirings/actor-flow/run-conversation.ts +36 -41
  79. package/src/wirings/ai-agent/ai-agent-agui.test.ts +0 -16
  80. package/src/wirings/ai-agent/ai-agent-agui.ts +0 -9
  81. package/src/wirings/ai-agent/ai-agent-prepare.ts +1 -2
  82. package/src/wirings/ai-agent/ai-agent.types.ts +0 -7
  83. package/src/wirings/cli/command-parser.test.ts +60 -0
  84. package/src/wirings/cli/command-parser.ts +12 -1
  85. package/src/wirings/rpc/rpc-runner.test.ts +28 -5
  86. package/src/wirings/rpc/rpc-runner.ts +1 -1
  87. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +86 -2
  88. package/src/wirings/workflow/feature.test.ts +131 -0
  89. package/src/wirings/workflow/feature.ts +78 -0
  90. package/src/wirings/workflow/graph/graph-runner.ts +3 -2
  91. package/src/wirings/workflow/graph/graph-validation.test.ts +1 -144
  92. package/src/wirings/workflow/graph/graph-validation.ts +0 -196
  93. package/src/wirings/workflow/graph/index.ts +1 -5
  94. package/src/wirings/workflow/index.ts +73 -6
  95. package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
  96. package/src/wirings/workflow/pikku-workflow-service.test.ts +55 -0
  97. package/src/wirings/workflow/pikku-workflow-service.ts +572 -419
  98. package/src/wirings/workflow/scenario-cookie-jar.test.ts +108 -0
  99. package/src/wirings/workflow/scenario-cookie-jar.ts +65 -0
  100. package/src/wirings/workflow/scenario-expectations.test.ts +153 -0
  101. package/src/wirings/workflow/scenario-hooks.test.ts +212 -0
  102. package/src/wirings/workflow/scenario-poll.test.ts +66 -0
  103. package/src/wirings/workflow/scenario-poll.ts +36 -0
  104. package/src/wirings/workflow/scenario-prose.test.ts +152 -0
  105. package/src/wirings/workflow/scenario-prose.ts +79 -0
  106. package/src/wirings/workflow/scenario-service.test.ts +155 -0
  107. package/src/wirings/workflow/scenario-step-guards.ts +43 -0
  108. package/src/wirings/workflow/scenario-step.test.ts +442 -9
  109. package/src/wirings/workflow/scenario-step.types.ts +157 -0
  110. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  111. package/src/wirings/workflow/workflow-dispatch-payload.test.ts +59 -0
  112. package/src/wirings/workflow/workflow-mirror.test.ts +178 -0
  113. package/src/wirings/workflow/workflow-replay-snapshot.test.ts +139 -0
  114. package/src/wirings/workflow/workflow-run-context.test.ts +177 -0
  115. package/src/wirings/workflow/workflow-run-polling.test.ts +132 -0
  116. package/src/wirings/workflow/workflow-step-ordinal.test.ts +4 -4
  117. package/src/wirings/workflow/workflow.types.ts +99 -5
  118. package/tsconfig.tsbuildinfo +1 -1
@@ -12,6 +12,7 @@ import type { PikkuScheduledTask } from '../wirings/scheduler/scheduler.types.js
12
12
  import type { PikkuQueue, QueueService } from '../wirings/queue/queue.types.js';
13
13
  import type { PikkuCLI } from '../wirings/cli/cli.types.js';
14
14
  import type { PikkuWorkflowWire, PikkuScenarioWire, WorkflowService, WorkflowServiceConfig, WorkflowStepWire } from '../wirings/workflow/workflow.types.js';
15
+ import type { PikkuBrowserWire, PikkuScenarioStepWire } from '../wirings/workflow/scenario-step.types.js';
15
16
  import type { PikkuGraphWire } from '../wirings/workflow/graph/workflow-graph.types.js';
16
17
  import type { PikkuTrigger } from '../wirings/trigger/trigger.types.js';
17
18
  import type { PikkuGateway } from '../wirings/gateway/gateway.types.js';
@@ -19,7 +20,7 @@ import type { SchedulerService } from '../services/scheduler-service.js';
19
20
  import type { DeploymentService } from '../services/deployment-service.js';
20
21
  import type { AIStorageService } from '../services/ai-storage-service.js';
21
22
  import type { ContentService } from '../services/content-service.js';
22
- import type { ScenarioActors } from '../services/scenario-actors-service.js';
23
+ import type { ScenarioActorOf, ScenarioActors } from '../services/scenario-actors-service.js';
23
24
  import type { AIAgentRunnerService } from '../services/ai-agent-runner-service.js';
24
25
  import type { AIEmbeddingService } from '../services/ai-embedding-service.js';
25
26
  import type { AIRunStateService } from '../services/ai-run-state-service.js';
@@ -85,6 +86,22 @@ export type FunctionRuntimeMeta = {
85
86
  scopes?: string[];
86
87
  expose?: boolean;
87
88
  remote?: boolean;
89
+ /**
90
+ * A step RPC: a name dispatched by a scenario run and refused everywhere
91
+ * else. It sits alongside `expose` (public) and `remote` as a kind of RPC
92
+ * rather than a separate concept — a step is invoked by name exactly as an
93
+ * RPC is, which is why a run records the step function in its `rpcName`.
94
+ * What makes it its own kind is that it is never network-callable: a step
95
+ * may drive a browser or assert against fixtures.
96
+ */
97
+ scenarioStep?: boolean;
98
+ /**
99
+ * The function behind a `pikkuScenario(...)` — a scenario's own body, as
100
+ * opposed to the steps it calls. Marked for the same reason as
101
+ * `scenarioStep`: a scenario is only ever run by `pikku scenario run`, so it
102
+ * has to be held back from the app bootstrap and from every deployed unit.
103
+ */
104
+ scenario?: boolean;
88
105
  mcp?: boolean;
89
106
  readonly?: boolean;
90
107
  deploy?: 'serverless' | 'server' | 'auto';
@@ -95,6 +112,10 @@ export type FunctionRuntimeMeta = {
95
112
  workflowRetries?: number;
96
113
  /** Timeout when this function is used as a workflow step (e.g. '30s', '5m'). */
97
114
  workflowTimeout?: string;
115
+ /** Scenario steps only: this step drives a browser, so the runner must provision one and an actor is mandatory. */
116
+ scenarioStepBrowser?: boolean;
117
+ /** Scenario steps only: the prose a reporter renders, with `{placeholders}` filled from the step's recorded input. */
118
+ scenarioStepTemplate?: string;
98
119
  version?: number;
99
120
  approvalRequired?: boolean;
100
121
  approvalDescription?: string;
@@ -306,7 +327,7 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
306
327
  /**
307
328
  * Represents different forms of wire within Pikku and the outside world.
308
329
  */
309
- export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boolean = false, UserSession extends CoreUserSession = CoreUserSession, TypedRPC extends PikkuRPC = PikkuRPC, IsChannel extends true | null = null, MCPTools extends string | never = never, TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire, TriggerOutput = unknown, TypedScenario extends PikkuScenarioWire | never = PikkuScenarioWire> = {
330
+ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boolean = false, UserSession extends CoreUserSession = CoreUserSession, TypedRPC extends PikkuRPC = PikkuRPC, IsChannel extends true | null = null, MCPTools extends string | never = never, TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire, TriggerOutput = unknown, TypedScenario extends PikkuScenarioWire | never = PikkuScenarioWire, TypedActors extends ScenarioActors = ScenarioActors> = {
310
331
  /** Always present — lazily initialised on first access for every function invocation */
311
332
  rpc: TypedRPC;
312
333
  } & Partial<{
@@ -326,7 +347,11 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
326
347
  cli: PikkuCLI;
327
348
  workflow: TypedWorkflow;
328
349
  scenario: TypedScenario;
329
- actors: ScenarioActors;
350
+ actors: TypedActors;
351
+ /** Present on every scenario step invocation */
352
+ scenarioStep: PikkuScenarioStepWire<ScenarioActorOf<TypedActors>>;
353
+ /** Present only when the runner provisioned a browser for this step */
354
+ browser: PikkuBrowserWire;
330
355
  workflowStep: WorkflowStepWire;
331
356
  graph: PikkuGraphWire;
332
357
  trigger: PikkuTrigger<TriggerOutput>;
@@ -9,7 +9,7 @@ import type { CoreAIAgent, AIAgentMeta } from '../wirings/ai-agent/ai-agent.type
9
9
  import type { CoreGateway, GatewaysMeta } from '../wirings/gateway/gateway.types.js';
10
10
  import type { CoreQueueWorker, QueueWorkersMeta } from '../wirings/queue/queue.types.js';
11
11
  import type { CoreScheduledTask, ScheduledTasksMeta } from '../wirings/scheduler/scheduler.types.js';
12
- import type { CoreWorkflow, WorkflowsRuntimeMeta } from '../wirings/workflow/workflow.types.js';
12
+ import type { CoreWorkflow, CoreFeature, WorkflowsRuntimeMeta } from '../wirings/workflow/workflow.types.js';
13
13
  import type { CoreTrigger, CoreTriggerSource, TriggerMeta, TriggerSourceMeta } from '../wirings/trigger/trigger.types.js';
14
14
  import type { FunctionsMeta, CorePikkuMiddleware, CorePikkuMiddlewareGroup, CreateConfig, CreateSingletonServices, CreateWireServices, CoreConfig, CoreSingletonServices, CoreServices, CoreUserSession } from './core.types.js';
15
15
  import type { CorePikkuChannelMiddleware } from '../wirings/channel/channel.types.js';
@@ -77,6 +77,8 @@ export interface PikkuPackageState {
77
77
  };
78
78
  workflows: {
79
79
  registrations: Map<string, CoreWorkflow>;
80
+ /** Scenario groups declared with `pikkuFeature`, keyed by export name. */
81
+ features: Map<string, CoreFeature>;
80
82
  meta: WorkflowsRuntimeMeta;
81
83
  };
82
84
  trigger: {
@@ -20,7 +20,7 @@ export interface ConverseOptions<TAgentName extends string = string> {
20
20
  evaluate: string;
21
21
  /** How the actor answers the agent's tool-approval requests. Default `'in-persona'`. */
22
22
  approvals?: ActorFlowApprovalPolicy;
23
- /** Model the persona uses for its own turns/decisions. Falls back to the actor service default. */
23
+ /** Model the actor uses for its own turns/decisions. Falls back to the actor service default. */
24
24
  model?: string;
25
25
  /** Hard cap on conversation turns before forcing evaluation. Default 12. */
26
26
  maxTurns?: number;
@@ -8,4 +8,4 @@
8
8
  * over HTTP.
9
9
  */
10
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';
11
+ export { runConversation, type RunConversationParams, type ActorLLM, } from './run-conversation.js';
@@ -1,20 +1,20 @@
1
1
  import type { ActorFlowApprovalPolicy, ActorFlowVerdict, TargetAgentDriver } from './actor-flow.types.js';
2
2
  import type { ScenarioActorConfig } from '../../services/scenario-actors-service.js';
3
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>;
4
+ /** The LLM call the actor uses for its own turns/decisions/evaluation. */
5
+ export type ActorLLM = (params: AIAgentRunnerParams) => Promise<AIAgentStepResult>;
6
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;
7
+ /** The actor's own config (personality/jobTitle/name), which shapes how it talks. */
8
+ actor: ScenarioActorConfig;
9
+ /** Stable actor name (for transcript labelling). */
10
+ actorName: string;
11
11
  /** What the actor is trying to get the target agent to accomplish. */
12
12
  task: string;
13
13
  /** Natural-language success criterion the actor evaluates at the end. */
14
14
  evaluate: string;
15
15
  /** How the actor answers the target agent's tool-approval requests. */
16
16
  approvals?: ActorFlowApprovalPolicy;
17
- /** Model the persona uses for its own turns/decisions. */
17
+ /** Model the actor uses for its own turns/decisions. */
18
18
  model: string;
19
19
  /** Hard cap on conversation turns. Default 12. */
20
20
  maxTurns?: number;
@@ -22,13 +22,13 @@ export interface RunConversationParams {
22
22
  maxApprovalRounds?: number;
23
23
  /** Transport that drives the target agent (HTTP in production). */
24
24
  target: TargetAgentDriver;
25
- /** The persona's own LLM. */
26
- llm: PersonaLLM;
25
+ /** The actor's own LLM. */
26
+ llm: ActorLLM;
27
27
  /** Display name of the target agent (transcript labelling). */
28
28
  agentName: string;
29
29
  }
30
30
  /**
31
- * Run a conversation: an LLM-driven persona holds a real multi-turn exchange
31
+ * Run a conversation: an LLM-driven actor holds a real multi-turn exchange
32
32
  * with a target agent (driven via the injected transport), answers the target's
33
33
  * tool-approval requests in-persona, then evaluates whether the task was met.
34
34
  * Deterministic checks are the caller's responsibility.
@@ -1,5 +1,5 @@
1
- /** One turn the persona takes: the message to send and whether it's finished. */
2
- const PERSONA_TURN_SCHEMA = {
1
+ /** One turn the actor takes: the message to send and whether it's finished. */
2
+ const ACTOR_TURN_SCHEMA = {
3
3
  type: 'object',
4
4
  properties: {
5
5
  message: { type: 'string' },
@@ -7,7 +7,7 @@ const PERSONA_TURN_SCHEMA = {
7
7
  },
8
8
  required: ['message', 'done'],
9
9
  };
10
- /** The persona's approve/deny decision for each pending tool request. */
10
+ /** The actor's approve/deny decision for each pending tool request. */
11
11
  const APPROVAL_DECISION_SCHEMA = {
12
12
  type: 'object',
13
13
  properties: {
@@ -26,7 +26,7 @@ const APPROVAL_DECISION_SCHEMA = {
26
26
  },
27
27
  required: ['decisions'],
28
28
  };
29
- /** The persona's final verdict on whether the task was accomplished. */
29
+ /** The actor's final verdict on whether the task was accomplished. */
30
30
  const EVALUATION_SCHEMA = {
31
31
  type: 'object',
32
32
  properties: {
@@ -60,13 +60,13 @@ function readObject(result) {
60
60
  }
61
61
  return null;
62
62
  }
63
- function personaInstructions(persona, task) {
63
+ function actorInstructions(actor, task) {
64
64
  return [
65
65
  `You are role-playing a real user interacting with an AI assistant. Stay in character at all times — you are the user, not the assistant.`,
66
- persona.name ? `Your name is ${persona.name}.` : '',
67
- persona.jobTitle ? `Your role: ${persona.jobTitle}.` : '',
68
- persona.personality
69
- ? `Your personality and communication style: ${persona.personality}. Match this tone, vocabulary, and level of detail exactly.`
66
+ actor.name ? `Your name is ${actor.name}.` : '',
67
+ actor.jobTitle ? `Your role: ${actor.jobTitle}.` : '',
68
+ actor.personality
69
+ ? `Your personality and communication style: ${actor.personality}. Match this tone, vocabulary, and level of detail exactly.`
70
70
  : '',
71
71
  `Your goal in this conversation: ${task}.`,
72
72
  `Send one message at a time. Set "done" to true only once your goal is clearly accomplished, or clearly impossible.`,
@@ -74,7 +74,7 @@ function personaInstructions(persona, task) {
74
74
  .filter(Boolean)
75
75
  .join('\n');
76
76
  }
77
- /** Route the target agent's pending tool approvals through the persona. */
77
+ /** Route the target agent's pending tool approvals through the actor. */
78
78
  async function decideApprovals(params, instructions, pending) {
79
79
  const policy = params.approvals ?? 'in-persona';
80
80
  if (policy === 'always') {
@@ -88,7 +88,7 @@ async function decideApprovals(params, instructions, pending) {
88
88
  .join('\n');
89
89
  const result = await params.llm({
90
90
  model: params.model,
91
- instructions: `${instructions}\nThe assistant is asking permission to run tools on your behalf. Decide whether YOU, as this persona, would allow each one.`,
91
+ instructions: `${instructions}\nThe assistant is asking permission to run tools on your behalf. Decide whether YOU, as this actor, would allow each one.`,
92
92
  messages: [
93
93
  msg('user', `The assistant wants to run these tools:\n${summary}\nApprove or deny each toolCallId.`),
94
94
  ],
@@ -104,7 +104,7 @@ async function decideApprovals(params, instructions, pending) {
104
104
  return { toolCallId: p.toolCallId, approved: match?.approved ?? false };
105
105
  });
106
106
  }
107
- /** Drive the target to a non-suspended reply, routing approvals to the persona. */
107
+ /** Drive the target to a non-suspended reply, routing approvals to the actor. */
108
108
  async function converseWithTarget(params, instructions, message) {
109
109
  const maxRounds = params.maxApprovalRounds ?? DEFAULT_MAX_APPROVAL_ROUNDS;
110
110
  let reply = await params.target.run(message);
@@ -122,40 +122,40 @@ async function converseWithTarget(params, instructions, message) {
122
122
  return reply;
123
123
  }
124
124
  /**
125
- * Run a conversation: an LLM-driven persona holds a real multi-turn exchange
125
+ * Run a conversation: an LLM-driven actor holds a real multi-turn exchange
126
126
  * with a target agent (driven via the injected transport), answers the target's
127
127
  * tool-approval requests in-persona, then evaluates whether the task was met.
128
128
  * Deterministic checks are the caller's responsibility.
129
129
  */
130
130
  export async function runConversation(params) {
131
131
  const maxTurns = params.maxTurns ?? DEFAULT_MAX_TURNS;
132
- const instructions = personaInstructions(params.persona, params.task);
133
- // Seed a kickoff so the very first persona turn has a non-empty message list
134
- // (providers reject an empty prompt). It's an instruction TO the persona, so
132
+ const instructions = actorInstructions(params.actor, params.task);
133
+ // Seed a kickoff so the very first actor turn has a non-empty message list
134
+ // (providers reject an empty prompt). It's an instruction TO the actor, so
135
135
  // it never appears in the transcript.
136
- const personaMessages = [
136
+ const actorMessages = [
137
137
  msg('user', 'Begin the conversation now — send your first message to the assistant to work towards your goal.'),
138
138
  ];
139
139
  const transcript = [];
140
140
  for (let turn = 0; turn < maxTurns; turn++) {
141
- const personaResult = await params.llm({
141
+ const actorResult = await params.llm({
142
142
  model: params.model,
143
143
  instructions,
144
- messages: personaMessages,
144
+ messages: actorMessages,
145
145
  tools: [],
146
146
  maxSteps: 1,
147
147
  toolChoice: 'none',
148
- outputSchema: PERSONA_TURN_SCHEMA,
148
+ outputSchema: ACTOR_TURN_SCHEMA,
149
149
  });
150
- const turnData = readObject(personaResult);
151
- const personaMessage = turnData?.message?.trim();
152
- if (!personaMessage) {
150
+ const turnData = readObject(actorResult);
151
+ const actorMessage = turnData?.message?.trim();
152
+ if (!actorMessage) {
153
153
  break;
154
154
  }
155
- personaMessages.push(msg('assistant', personaMessage));
156
- transcript.push(`${params.personaName}: ${personaMessage}`);
157
- const reply = await converseWithTarget(params, instructions, personaMessage);
158
- personaMessages.push(msg('user', reply.text ?? ''));
155
+ actorMessages.push(msg('assistant', actorMessage));
156
+ transcript.push(`${params.actorName}: ${actorMessage}`);
157
+ const reply = await converseWithTarget(params, instructions, actorMessage);
158
+ actorMessages.push(msg('user', reply.text ?? ''));
159
159
  transcript.push(`${params.agentName}: ${reply.text ?? ''}`);
160
160
  if (turnData?.done) {
161
161
  break;
@@ -256,14 +256,6 @@ export function wrapChannelWithAGUI(inner, options) {
256
256
  });
257
257
  break;
258
258
  }
259
- case 'workflow-created': {
260
- send({
261
- type: 'CUSTOM',
262
- name: 'pikku:workflow-created',
263
- value: { workflowName: event.workflowName, graph: event.graph },
264
- });
265
- break;
266
- }
267
259
  case 'agent-call': {
268
260
  send({
269
261
  type: 'CUSTOM',
@@ -312,8 +312,7 @@ export function createScopedChannel(parent, agentName, session) {
312
312
  event.type === 'tool-call' ||
313
313
  event.type === 'tool-result' ||
314
314
  event.type === 'usage' ||
315
- event.type === 'error' ||
316
- event.type === 'workflow-created') {
315
+ event.type === 'error') {
317
316
  parent.send({ ...event, agent: agentName, session });
318
317
  }
319
318
  else {
@@ -346,12 +346,6 @@ export type AIStreamEvent = {
346
346
  type: 'audio-done';
347
347
  agent?: string;
348
348
  session?: string;
349
- } | {
350
- type: 'workflow-created';
351
- workflowName: string;
352
- graph: any;
353
- agent?: string;
354
- session?: string;
355
349
  } | {
356
350
  type: 'data';
357
351
  name: string;
@@ -150,8 +150,18 @@ export function parseCLIArguments(args, programName, allMeta) {
150
150
  const arg = args[currentIndex];
151
151
  if (arg.startsWith('--')) {
152
152
  // Long option (--from-plan → fromPlan)
153
+ const negatedKey = arg.startsWith('--no-')
154
+ ? toCamelCase(arg.slice(5))
155
+ : undefined;
153
156
  const equalIndex = arg.indexOf('=');
154
- if (equalIndex > 0) {
157
+ if (negatedKey &&
158
+ typeof availableOptions[negatedKey]?.default === 'boolean') {
159
+ // --no-<flag> turns a boolean option off. Only options that declare a
160
+ // boolean default can be negated, so a literal `--no-something` option
161
+ // name still parses as itself.
162
+ optionArgs[negatedKey] = false;
163
+ }
164
+ else if (equalIndex > 0) {
155
165
  // --option=value format
156
166
  const key = toCamelCase(arg.slice(2, equalIndex));
157
167
  const optionDef = availableOptions[key];
@@ -129,7 +129,7 @@ export class ContextAwareRPCService {
129
129
  if (!functionMeta) {
130
130
  throw new RPCNotFoundError(funcName);
131
131
  }
132
- if (!functionMeta.expose) {
132
+ if (!functionMeta.expose || functionMeta.scenarioStep) {
133
133
  throw new RPCNotFoundError(funcName);
134
134
  }
135
135
  return await this.rpc(funcName, data);
@@ -5,6 +5,7 @@
5
5
  import type { StandardSchemaV1 } from '@standard-schema/spec';
6
6
  import type { WorkflowRun } from '../workflow.types.js';
7
7
  import type { ScenarioActor } from '../../../services/scenario-actors-service.js';
8
+ import type { ScenarioStepOptions, ScenarioStepPhase } from '../scenario-step.types.js';
8
9
  /**
9
10
  * Workflow step options
10
11
  */
@@ -60,6 +61,13 @@ export type WorkflowWireDoRPC = <TOutput = any, TInput = any>(stepName: string,
60
61
  * Type signature for workflow.do() inline form - used by inspector
61
62
  */
62
63
  export type WorkflowWireDoInline = <T>(stepName: string, fn: () => Promise<T> | T, options?: WorkflowStepOptions) => Promise<T>;
64
+ /**
65
+ * Type signature for scenario.step/given/when/then - used by inspector.
66
+ *
67
+ * Deliberately mirrors WorkflowWireDoRPC's shape: the target is a string, not
68
+ * an imported symbol, so the extractor reads it as a literal.
69
+ */
70
+ export type ScenarioStepInvocation = <TOutput = any, TInput = any>(stepName: string, stepFunc: string, data?: TInput, options?: ScenarioStepOptions) => Promise<TOutput>;
63
71
  /**
64
72
  * Type signature for workflow.sleep() - used by inspector
65
73
  */
@@ -172,6 +180,33 @@ export interface RpcStepMeta {
172
180
  /** True for workflow.expectEventually polling steps */
173
181
  expectEventually?: boolean;
174
182
  }
183
+ /**
184
+ * Scenario step metadata — a call to `scenario.step/given/when/then`.
185
+ *
186
+ * Distinct from RpcStepMeta on purpose: a step runs locally through
187
+ * runPikkuFunc and must never be treated as dispatchable on the queue/replay
188
+ * path, nor registered as a callable RPC (a browser-driving step must not be
189
+ * network-invocable).
190
+ */
191
+ export interface ScenarioStepMeta {
192
+ type: 'scenarioStep';
193
+ /** Cache key (first argument), ordinal-suffixed by the engine when repeated */
194
+ stepName: string;
195
+ /** Registered name of the step function being run */
196
+ stepFunc: string;
197
+ /** Which keyword the reporter prefixes — given/when/then, or none for `step` */
198
+ phase: ScenarioStepPhase;
199
+ /** Output variable name (if assigned) */
200
+ outputVar?: string;
201
+ /** Input source mappings, or 'passthrough' when entire data is passed */
202
+ inputs?: Record<string, InputSource> | 'passthrough';
203
+ /** Step options */
204
+ options?: WorkflowStepOptions;
205
+ /** Scenario actor name this step runs as ({ actor: actors.x }) */
206
+ actor?: string;
207
+ /** Mirrors the step function's `browser: true` declaration */
208
+ browser?: boolean;
209
+ }
175
210
  /**
176
211
  * Simple condition expression (leaf node)
177
212
  */
@@ -214,7 +249,7 @@ export interface BranchStepMeta {
214
249
  export interface ParallelGroupStepMeta {
215
250
  type: 'parallel';
216
251
  /** Child steps to execute in parallel */
217
- children: RpcStepMeta[];
252
+ children: Array<RpcStepMeta | ScenarioStepMeta>;
218
253
  }
219
254
  /**
220
255
  * Fanout step metadata (parallel or sequential iteration)
@@ -234,7 +269,7 @@ export interface FanoutStepMeta {
234
269
  /** Execution mode */
235
270
  mode: 'parallel' | 'sequential';
236
271
  /** Steps to execute inline per iteration, in order */
237
- body: Array<RpcStepMeta | SleepStepMeta | SuspendStepMeta>;
272
+ body: Array<RpcStepMeta | SleepStepMeta | SuspendStepMeta | ScenarioStepMeta>;
238
273
  /** Time between iterations (sequential mode only) */
239
274
  timeBetween?: string;
240
275
  }
@@ -385,7 +420,7 @@ export interface ArrayPredicateStepMeta {
385
420
  /**
386
421
  * Workflow step metadata (extracted by inspector)
387
422
  */
388
- export type WorkflowStepMeta = RpcStepMeta | BranchStepMeta | ParallelGroupStepMeta | FanoutStepMeta | ReturnStepMeta | InlineStepMeta | SleepStepMeta | CancelStepMeta | SuspendStepMeta | ApprovalStepMeta | SwitchStepMeta | FilterStepMeta | ArrayPredicateStepMeta | SetStepMeta;
423
+ export type WorkflowStepMeta = RpcStepMeta | ScenarioStepMeta | BranchStepMeta | ParallelGroupStepMeta | FanoutStepMeta | ReturnStepMeta | InlineStepMeta | SleepStepMeta | CancelStepMeta | SuspendStepMeta | ApprovalStepMeta | SwitchStepMeta | FilterStepMeta | ArrayPredicateStepMeta | SetStepMeta;
389
424
  /**
390
425
  * Workflow step wire context for RPC functions
391
426
  * Provides step-level metadata including retry attempt tracking
@@ -448,5 +483,19 @@ export interface PikkuScenarioWire extends PikkuWorkflowWire {
448
483
  expectError: <TInput = any>(stepName: string, rpcName: string, data: TInput, options?: WorkflowExpectErrorOptions) => Promise<string>;
449
484
  /** Stub-assertion step: asserts `service.method` was called on the target server */
450
485
  expectService: (stepName: string, serviceMethod: string, options?: WorkflowExpectServiceOptions) => Promise<void>;
486
+ /**
487
+ * Run a registered scenario step. Shaped exactly like `do`'s RPC form —
488
+ * `(stepName, target, data, options)` — so the inspector reads the target as
489
+ * a string literal rather than resolving an imported symbol.
490
+ *
491
+ * The generated `TypedScenario` narrows these over `FlattenedScenarioStepMap`.
492
+ */
493
+ step(stepName: string, stepFunc: string, data?: any, options?: ScenarioStepOptions): Promise<any>;
494
+ /** `step` with a "Given" prefix in the rendered prose */
495
+ given(stepName: string, stepFunc: string, data?: any, options?: ScenarioStepOptions): Promise<any>;
496
+ /** `step` with a "When" prefix in the rendered prose */
497
+ when(stepName: string, stepFunc: string, data?: any, options?: ScenarioStepOptions): Promise<any>;
498
+ /** `step` with a "Then" prefix in the rendered prose */
499
+ then(stepName: string, stepFunc: string, data?: any, options?: ScenarioStepOptions): Promise<any>;
451
500
  runScheduledTask: (name: string) => Promise<unknown>;
452
501
  }
@@ -0,0 +1,28 @@
1
+ import type { CoreFeature, CoreWorkflow, FeaturePlanEntry } from './workflow.types.js';
2
+ /**
3
+ * Register a feature under the name it is exported as. Called from generated
4
+ * wiring, the same way `addWorkflow` is.
5
+ */
6
+ export declare const addFeature: (featureId: string, feature: CoreFeature, packageName?: string | null) => void;
7
+ /**
8
+ * Resolve every feature's scenario references back to the names their
9
+ * scenarios are registered under.
10
+ *
11
+ * Matching is by **object identity**: `pikkuScenario` returns its config
12
+ * verbatim and `addWorkflow` registers that same object, so a feature holding
13
+ * the imported identifier holds the very object that was registered. Nothing
14
+ * is matched by shape, by name, or by any other guess — which is also why a
15
+ * scenario built inline inside a feature (and therefore never registered)
16
+ * comes back as unresolved rather than silently running as something else.
17
+ *
18
+ * Entries are returned in declaration order, features in registration order.
19
+ * A scenario's effective tags are its own plus the containing feature's, so
20
+ * `--tags credential` selects through the feature.
21
+ */
22
+ export declare const resolveFeatureScenarios: (features: Map<string, CoreFeature>, registrations: Map<string, CoreWorkflow>) => {
23
+ entries: FeaturePlanEntry[];
24
+ unresolved: Array<{
25
+ featureId: string;
26
+ index: number;
27
+ }>;
28
+ };
@@ -0,0 +1,57 @@
1
+ import { pikkuState } from '../../pikku-state.js';
2
+ /**
3
+ * Register a feature under the name it is exported as. Called from generated
4
+ * wiring, the same way `addWorkflow` is.
5
+ */
6
+ export const addFeature = (featureId, feature, packageName = null) => {
7
+ pikkuState(packageName, 'workflows', 'features').set(featureId, feature);
8
+ };
9
+ /**
10
+ * Resolve every feature's scenario references back to the names their
11
+ * scenarios are registered under.
12
+ *
13
+ * Matching is by **object identity**: `pikkuScenario` returns its config
14
+ * verbatim and `addWorkflow` registers that same object, so a feature holding
15
+ * the imported identifier holds the very object that was registered. Nothing
16
+ * is matched by shape, by name, or by any other guess — which is also why a
17
+ * scenario built inline inside a feature (and therefore never registered)
18
+ * comes back as unresolved rather than silently running as something else.
19
+ *
20
+ * Entries are returned in declaration order, features in registration order.
21
+ * A scenario's effective tags are its own plus the containing feature's, so
22
+ * `--tags credential` selects through the feature.
23
+ */
24
+ export const resolveFeatureScenarios = (features, registrations) => {
25
+ const nameByConfig = new Map();
26
+ for (const [name, registration] of registrations) {
27
+ nameByConfig.set(registration.func, name);
28
+ }
29
+ const entries = [];
30
+ const unresolved = [];
31
+ for (const [featureId, feature] of features) {
32
+ const scenarios = feature.scenarios ?? [];
33
+ for (let index = 0; index < scenarios.length; index++) {
34
+ const entry = scenarios[index];
35
+ const paired = typeof entry === 'object' && entry !== null && 'scenario' in entry;
36
+ const config = paired ? entry.scenario : entry;
37
+ const scenarioName = nameByConfig.get(config);
38
+ if (!scenarioName) {
39
+ unresolved.push({ featureId, index });
40
+ continue;
41
+ }
42
+ entries.push({
43
+ featureId,
44
+ featureName: feature.name ?? featureId,
45
+ scenarioName,
46
+ data: paired ? entry.data : undefined,
47
+ tags: [
48
+ ...new Set([
49
+ ...(registrations.get(scenarioName)?.func?.tags ?? []),
50
+ ...(feature.tags ?? []),
51
+ ]),
52
+ ],
53
+ });
54
+ }
55
+ }
56
+ return { entries, unresolved };
57
+ };
@@ -399,8 +399,9 @@ export async function continueGraph(workflowService, runId, graphName, overrideM
399
399
  }
400
400
  return;
401
401
  }
402
- const run = await workflowService.getRun(runId);
403
- const triggerInput = run?.input;
402
+ // The same run read a few lines up: nothing between here and there writes it,
403
+ // and `input` is fixed at creation anyway.
404
+ const triggerInput = currentRun?.input;
404
405
  for (const fire of plan.toFire) {
405
406
  const node = nodes[fire.logical];
406
407
  if (!node?.rpcName)
@@ -1,3 +1 @@
1
- export declare function computeEntryNodeIds(nodes: Record<string, any>): string[];
2
- export declare function validateWorkflowWiring(nodes: Record<string, any>, toolNames: string[]): string[];
3
1
  export declare function generateMermaidDiagram(workflowName: string, nodes: Record<string, any>, entryNodeIds: string[]): string;