@pikku/core 0.12.50 → 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.
- package/CHANGELOG.md +93 -0
- package/dist/index.d.ts +1 -1
- package/dist/services/http-scenario-actors.d.ts +67 -0
- package/dist/services/http-scenario-actors.js +193 -0
- package/dist/services/http-user-flow-actors.d.ts +20 -0
- package/dist/services/http-user-flow-actors.js +100 -0
- package/dist/services/index.d.ts +2 -2
- package/dist/services/index.js +1 -1
- package/dist/services/meta-service.d.ts +4 -4
- package/dist/services/meta-service.js +8 -8
- package/dist/services/scenario-actors-service.d.ts +39 -0
- package/dist/services/scenario-actors-service.js +1 -0
- package/dist/services/user-flow-actors-service.d.ts +11 -1
- package/dist/types/core.types.d.ts +48 -4
- package/dist/wirings/actor-flow/actor-flow.types.d.ts +68 -0
- package/dist/wirings/actor-flow/actor-flow.types.js +1 -0
- package/dist/wirings/actor-flow/index.d.ts +11 -0
- package/dist/wirings/actor-flow/index.js +1 -0
- package/dist/wirings/actor-flow/run-conversation.d.ts +36 -0
- package/dist/wirings/actor-flow/run-conversation.js +181 -0
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +6 -6
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +10 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +42 -2
- package/dist/wirings/workflow/workflow.types.d.ts +6 -6
- package/package.json +2 -1
- package/run-tests.sh +4 -4
- package/src/index.ts +6 -0
- package/src/services/http-scenario-actors-converse.test.ts +222 -0
- package/src/services/{http-user-flow-actors.test.ts → http-scenario-actors.test.ts} +17 -7
- package/src/services/http-scenario-actors.ts +269 -0
- package/src/services/index.ts +8 -8
- package/src/services/meta-service.ts +9 -9
- package/src/services/{user-flow-actors-service.ts → scenario-actors-service.ts} +18 -4
- package/src/types/core.types.ts +53 -4
- package/src/wirings/actor-flow/actor-flow.types.ts +73 -0
- package/src/wirings/actor-flow/index.ts +22 -0
- package/src/wirings/actor-flow/run-conversation.test.ts +176 -0
- package/src/wirings/actor-flow/run-conversation.ts +285 -0
- package/src/wirings/http/http.types.ts +1 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +6 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +50 -5
- package/src/wirings/workflow/{user-flow-step.test.ts → scenario-step.test.ts} +19 -14
- package/src/wirings/workflow/workflow.types.ts +6 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/src/services/http-user-flow-actors.ts +0 -132
|
@@ -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
|
|
@@ -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 {
|
|
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';
|
|
@@ -186,7 +186,7 @@ export type CoreConfig<Config extends Record<string, unknown> = {}> = {
|
|
|
186
186
|
export interface CoreUserSession {
|
|
187
187
|
userId?: string;
|
|
188
188
|
orgId?: string;
|
|
189
|
-
/** True when the session belongs to a synthetic
|
|
189
|
+
/** True when the session belongs to a synthetic scenario actor — lets audits/analytics address synthetic traffic */
|
|
190
190
|
actor?: boolean;
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
@@ -266,8 +266,8 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
|
|
|
266
266
|
queue: PikkuQueue;
|
|
267
267
|
cli: PikkuCLI;
|
|
268
268
|
workflow: TypedWorkflow;
|
|
269
|
-
/**
|
|
270
|
-
actors:
|
|
269
|
+
/** Scenario actor registry (scenario runs only) — pass into workflow.do as `{ actor: actors.x }` */
|
|
270
|
+
actors: ScenarioActors;
|
|
271
271
|
workflowStep: WorkflowStepWire;
|
|
272
272
|
graph: PikkuGraphWire;
|
|
273
273
|
trigger: PikkuTrigger<TriggerOutput>;
|
|
@@ -425,6 +425,50 @@ export type CommonWireMeta = {
|
|
|
425
425
|
middleware?: MiddlewareMetadata[];
|
|
426
426
|
permissions?: PermissionMetadata[];
|
|
427
427
|
};
|
|
428
|
+
/**
|
|
429
|
+
* Dependency security audit artifact — the shape of `.pikku/audit.json` written
|
|
430
|
+
* by `pikku audit`. Canonical home for the type shared by the CLI (writer), the
|
|
431
|
+
* console addon (reader), and the console UI (renderer).
|
|
432
|
+
*/
|
|
433
|
+
export type SecuritySeverity = 'critical' | 'high' | 'moderate' | 'low' | 'info';
|
|
434
|
+
export type SecurityUpdateLevel = 'major' | 'minor' | 'patch' | 'unknown';
|
|
435
|
+
export interface SecurityAuditIssue {
|
|
436
|
+
package: string;
|
|
437
|
+
severity: SecuritySeverity;
|
|
438
|
+
title: string;
|
|
439
|
+
advisoryId: string;
|
|
440
|
+
url: string;
|
|
441
|
+
vulnerableVersions: string;
|
|
442
|
+
cwe: string[];
|
|
443
|
+
cvssScore: number | null;
|
|
444
|
+
recommendedVersion: string | null;
|
|
445
|
+
}
|
|
446
|
+
export interface SecurityAuditUpdate {
|
|
447
|
+
package: string;
|
|
448
|
+
current: string;
|
|
449
|
+
latest: string;
|
|
450
|
+
level: SecurityUpdateLevel;
|
|
451
|
+
}
|
|
452
|
+
export interface SecurityAuditSummary {
|
|
453
|
+
totalIssues: number;
|
|
454
|
+
critical: number;
|
|
455
|
+
high: number;
|
|
456
|
+
moderate: number;
|
|
457
|
+
low: number;
|
|
458
|
+
totalUpdates: number;
|
|
459
|
+
major: number;
|
|
460
|
+
minor: number;
|
|
461
|
+
patch: number;
|
|
462
|
+
}
|
|
463
|
+
export interface SecurityAuditReport {
|
|
464
|
+
schemaVersion: number;
|
|
465
|
+
tool: string;
|
|
466
|
+
generatedAt: string;
|
|
467
|
+
note?: string;
|
|
468
|
+
issues: SecurityAuditIssue[];
|
|
469
|
+
updates: SecurityAuditUpdate[];
|
|
470
|
+
summary: SecurityAuditSummary;
|
|
471
|
+
}
|
|
428
472
|
/**
|
|
429
473
|
* Serialized error for storage
|
|
430
474
|
*/
|
|
@@ -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>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/** One turn the persona takes: the message to send and whether it's finished. */
|
|
2
|
+
const PERSONA_TURN_SCHEMA = {
|
|
3
|
+
type: 'object',
|
|
4
|
+
properties: {
|
|
5
|
+
message: { type: 'string' },
|
|
6
|
+
done: { type: 'boolean' },
|
|
7
|
+
},
|
|
8
|
+
required: ['message', 'done'],
|
|
9
|
+
};
|
|
10
|
+
/** The persona's approve/deny decision for each pending tool request. */
|
|
11
|
+
const APPROVAL_DECISION_SCHEMA = {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
decisions: {
|
|
15
|
+
type: 'array',
|
|
16
|
+
items: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
toolCallId: { type: 'string' },
|
|
20
|
+
approved: { type: 'boolean' },
|
|
21
|
+
reason: { type: 'string' },
|
|
22
|
+
},
|
|
23
|
+
required: ['toolCallId', 'approved'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ['decisions'],
|
|
28
|
+
};
|
|
29
|
+
/** The persona's final verdict on whether the task was accomplished. */
|
|
30
|
+
const EVALUATION_SCHEMA = {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
passed: { type: 'boolean' },
|
|
34
|
+
reasoning: { type: 'string' },
|
|
35
|
+
},
|
|
36
|
+
required: ['passed', 'reasoning'],
|
|
37
|
+
};
|
|
38
|
+
const DEFAULT_MAX_TURNS = 12;
|
|
39
|
+
const DEFAULT_MAX_APPROVAL_ROUNDS = 16;
|
|
40
|
+
function msg(role, content) {
|
|
41
|
+
return {
|
|
42
|
+
id: globalThis.crypto.randomUUID(),
|
|
43
|
+
role,
|
|
44
|
+
content,
|
|
45
|
+
createdAt: new Date(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Read a structured `run` result, falling back to parsing JSON from text. */
|
|
49
|
+
function readObject(result) {
|
|
50
|
+
if (result.object && typeof result.object === 'object') {
|
|
51
|
+
return result.object;
|
|
52
|
+
}
|
|
53
|
+
if (result.text) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(result.text);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function personaInstructions(persona, task) {
|
|
64
|
+
return [
|
|
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.`
|
|
70
|
+
: '',
|
|
71
|
+
`Your goal in this conversation: ${task}.`,
|
|
72
|
+
`Send one message at a time. Set "done" to true only once your goal is clearly accomplished, or clearly impossible.`,
|
|
73
|
+
]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join('\n');
|
|
76
|
+
}
|
|
77
|
+
/** Route the target agent's pending tool approvals through the persona. */
|
|
78
|
+
async function decideApprovals(params, instructions, pending) {
|
|
79
|
+
const policy = params.approvals ?? 'in-persona';
|
|
80
|
+
if (policy === 'always') {
|
|
81
|
+
return pending.map((p) => ({ toolCallId: p.toolCallId, approved: true }));
|
|
82
|
+
}
|
|
83
|
+
if (policy === 'never') {
|
|
84
|
+
return pending.map((p) => ({ toolCallId: p.toolCallId, approved: false }));
|
|
85
|
+
}
|
|
86
|
+
const summary = pending
|
|
87
|
+
.map((p) => `- toolCallId "${p.toolCallId}": ${p.toolName}(${JSON.stringify(p.args)})${p.reason ? ` — ${p.reason}` : ''}`)
|
|
88
|
+
.join('\n');
|
|
89
|
+
const result = await params.llm({
|
|
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.`,
|
|
92
|
+
messages: [
|
|
93
|
+
msg('user', `The assistant wants to run these tools:\n${summary}\nApprove or deny each toolCallId.`),
|
|
94
|
+
],
|
|
95
|
+
tools: [],
|
|
96
|
+
maxSteps: 1,
|
|
97
|
+
toolChoice: 'none',
|
|
98
|
+
outputSchema: APPROVAL_DECISION_SCHEMA,
|
|
99
|
+
});
|
|
100
|
+
const decided = readObject(result)?.decisions ?? [];
|
|
101
|
+
// Every pending call must get a decision; a missing one defaults to denied.
|
|
102
|
+
return pending.map((p) => {
|
|
103
|
+
const match = decided.find((d) => d.toolCallId === p.toolCallId);
|
|
104
|
+
return { toolCallId: p.toolCallId, approved: match?.approved ?? false };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Drive the target to a non-suspended reply, routing approvals to the persona. */
|
|
108
|
+
async function converseWithTarget(params, instructions, message) {
|
|
109
|
+
const maxRounds = params.maxApprovalRounds ?? DEFAULT_MAX_APPROVAL_ROUNDS;
|
|
110
|
+
let reply = await params.target.run(message);
|
|
111
|
+
let rounds = 0;
|
|
112
|
+
while (reply.status === 'suspended' &&
|
|
113
|
+
reply.pendingApprovals &&
|
|
114
|
+
reply.pendingApprovals.length > 0) {
|
|
115
|
+
if (rounds >= maxRounds) {
|
|
116
|
+
throw new Error(`Target agent "${params.agentName}" stayed suspended after ${maxRounds} approval rounds (runId ${reply.runId}); aborting to avoid an unbounded approve loop.`);
|
|
117
|
+
}
|
|
118
|
+
rounds++;
|
|
119
|
+
const decisions = await decideApprovals(params, instructions, reply.pendingApprovals);
|
|
120
|
+
reply = await params.target.approve(reply.runId, decisions);
|
|
121
|
+
}
|
|
122
|
+
return reply;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run a conversation: an LLM-driven persona holds a real multi-turn exchange
|
|
126
|
+
* with a target agent (driven via the injected transport), answers the target's
|
|
127
|
+
* tool-approval requests in-persona, then evaluates whether the task was met.
|
|
128
|
+
* Deterministic checks are the caller's responsibility.
|
|
129
|
+
*/
|
|
130
|
+
export async function runConversation(params) {
|
|
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
|
|
135
|
+
// it never appears in the transcript.
|
|
136
|
+
const personaMessages = [
|
|
137
|
+
msg('user', 'Begin the conversation now — send your first message to the assistant to work towards your goal.'),
|
|
138
|
+
];
|
|
139
|
+
const transcript = [];
|
|
140
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
141
|
+
const personaResult = await params.llm({
|
|
142
|
+
model: params.model,
|
|
143
|
+
instructions,
|
|
144
|
+
messages: personaMessages,
|
|
145
|
+
tools: [],
|
|
146
|
+
maxSteps: 1,
|
|
147
|
+
toolChoice: 'none',
|
|
148
|
+
outputSchema: PERSONA_TURN_SCHEMA,
|
|
149
|
+
});
|
|
150
|
+
const turnData = readObject(personaResult);
|
|
151
|
+
const personaMessage = turnData?.message?.trim();
|
|
152
|
+
if (!personaMessage) {
|
|
153
|
+
break;
|
|
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 ?? ''));
|
|
159
|
+
transcript.push(`${params.agentName}: ${reply.text ?? ''}`);
|
|
160
|
+
if (turnData?.done) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const evalResult = await params.llm({
|
|
165
|
+
model: params.model,
|
|
166
|
+
instructions: `${instructions}\nThe conversation has ended. Judge honestly whether your goal was accomplished.`,
|
|
167
|
+
messages: [
|
|
168
|
+
msg('user', `Here is the full conversation:\n\n${transcript.join('\n')}\n\nSuccess criterion: ${params.evaluate}\n\nWas it met?`),
|
|
169
|
+
],
|
|
170
|
+
tools: [],
|
|
171
|
+
maxSteps: 1,
|
|
172
|
+
toolChoice: 'none',
|
|
173
|
+
outputSchema: EVALUATION_SCHEMA,
|
|
174
|
+
});
|
|
175
|
+
const verdict = readObject(evalResult);
|
|
176
|
+
return {
|
|
177
|
+
passed: verdict?.passed ?? false,
|
|
178
|
+
reasoning: verdict?.reasoning ?? 'No evaluation produced.',
|
|
179
|
+
transcript,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* These types define the step-based workflow format extracted by the inspector
|
|
4
4
|
*/
|
|
5
5
|
import type { WorkflowRun } from '../workflow.types.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { ScenarioActor } from '../../../services/scenario-actors-service.js';
|
|
7
7
|
/**
|
|
8
8
|
* Workflow step options
|
|
9
9
|
*/
|
|
@@ -15,16 +15,16 @@ export interface WorkflowStepOptions {
|
|
|
15
15
|
/** Delay between retry attempts (e.g., '1s', '2s', '2min') */
|
|
16
16
|
retryDelay?: string | number;
|
|
17
17
|
/**
|
|
18
|
-
* Run this step as an actor (
|
|
18
|
+
* Run this step as an actor (scenarios). The RPC is sent through the
|
|
19
19
|
* actor's authenticated client over the REAL transport — never dispatched
|
|
20
20
|
* internally — so auth middleware and permissions are exercised end-to-end.
|
|
21
21
|
* The step is recorded durably like any RPC step.
|
|
22
22
|
*/
|
|
23
|
-
actor?:
|
|
23
|
+
actor?: ScenarioActor;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Options for workflow.expectEventually() — a durable polling step used by
|
|
27
|
-
*
|
|
27
|
+
* scenarios to await async effects (a notification landing, a job finishing).
|
|
28
28
|
*/
|
|
29
29
|
export interface WorkflowExpectEventuallyOptions extends WorkflowStepOptions {
|
|
30
30
|
/** Give up after this long (e.g. '30s'). Default '30s'. */
|
|
@@ -110,7 +110,7 @@ export interface RpcStepMeta {
|
|
|
110
110
|
inputs?: Record<string, InputSource> | 'passthrough';
|
|
111
111
|
/** Step options */
|
|
112
112
|
options?: WorkflowStepOptions;
|
|
113
|
-
/**
|
|
113
|
+
/** Scenario actor name this step runs as ({ actor: actors.x }) */
|
|
114
114
|
actor?: string;
|
|
115
115
|
/** True for workflow.expectEventually polling steps */
|
|
116
116
|
expectEventually?: boolean;
|
|
@@ -340,7 +340,7 @@ export interface PikkuWorkflowWire {
|
|
|
340
340
|
/** Execute a workflow step (overloaded - RPC or inline form) */
|
|
341
341
|
do: WorkflowWireDoRPC & WorkflowWireDoInline;
|
|
342
342
|
/**
|
|
343
|
-
* Durable polling step (
|
|
343
|
+
* Durable polling step (scenarios): invoke `rpcName` (as an actor when
|
|
344
344
|
* `options.as` is set) until `predicate` passes or `options.within` elapses.
|
|
345
345
|
*/
|
|
346
346
|
expectEventually: <TOutput = any, TInput = any>(stepName: string, rpcName: string, data: TInput, predicate: (output: TOutput) => boolean, options?: WorkflowExpectEventuallyOptions) => Promise<TOutput>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SerializedError } from '../../types/core.types.js';
|
|
2
2
|
import type { PikkuWorkflowWire, StepState, StepStatus, WorkflowPlannedStep, WorkflowRun, WorkflowRunMirror, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
|
|
3
3
|
import type { WorkflowService } from '../../services/workflow-service.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { ScenarioActors } from '../../services/scenario-actors-service.js';
|
|
5
5
|
import { PikkuError } from '../../errors/error-handler.js';
|
|
6
6
|
import { type RunTimeline, type ReconstructedRunState } from './run-timeline.js';
|
|
7
7
|
import type { JobOptions } from '../queue/queue.types.js';
|
|
@@ -366,6 +366,14 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
366
366
|
* false to fall through to inline `setTimeout` behavior.
|
|
367
367
|
*/
|
|
368
368
|
protected scheduleSleep(runId: string, stepId: string, duration: number | string): Promise<boolean>;
|
|
369
|
+
/** Build HTTP scenario actors for a run started without them; undefined when SCENARIO_ACTOR_SECRET or the API URL is missing */
|
|
370
|
+
private resolveScenarioActors;
|
|
371
|
+
/**
|
|
372
|
+
* Start a new workflow run
|
|
373
|
+
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
374
|
+
* @param options.inline - If true, execute workflow directly without queue service
|
|
375
|
+
* @param options.startNode - Starting node ID for graph workflows (from wire config)
|
|
376
|
+
*/
|
|
369
377
|
/**
|
|
370
378
|
* Start a new workflow run
|
|
371
379
|
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
@@ -375,7 +383,7 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
375
383
|
startWorkflow<I>(name: string, input: I, wire: WorkflowRunWire, rpcService: any, options?: {
|
|
376
384
|
inline?: boolean;
|
|
377
385
|
startNode?: string;
|
|
378
|
-
actors?:
|
|
386
|
+
actors?: ScenarioActors;
|
|
379
387
|
}): Promise<{
|
|
380
388
|
runId: string;
|
|
381
389
|
}>;
|
|
@@ -3,6 +3,7 @@ import { pikkuWorkflowWorkerFunc, pikkuWorkflowOrchestratorFunc, pikkuWorkflowSl
|
|
|
3
3
|
import { wireQueueWorker } from '../queue/queue-runner.js';
|
|
4
4
|
import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../pikku-state.js';
|
|
5
5
|
import { getDurationInMilliseconds } from '../../time-utils.js';
|
|
6
|
+
import { createHttpScenarioActors } from '../../services/http-scenario-actors.js';
|
|
6
7
|
const resolveWorkflowMeta = (name) => {
|
|
7
8
|
const rootMeta = pikkuState(null, 'workflows', 'meta');
|
|
8
9
|
if (rootMeta[name]) {
|
|
@@ -601,6 +602,41 @@ export class PikkuWorkflowService {
|
|
|
601
602
|
await getSingletonServices().schedulerService.scheduleRPC(duration, this.getConfig().sleeperRPCName, { runId, stepId });
|
|
602
603
|
return true;
|
|
603
604
|
}
|
|
605
|
+
/** Build HTTP scenario actors for a run started without them; undefined when SCENARIO_ACTOR_SECRET or the API URL is missing */
|
|
606
|
+
async resolveScenarioActors() {
|
|
607
|
+
const services = getSingletonServices();
|
|
608
|
+
const variables = services?.variables;
|
|
609
|
+
const metaService = services?.metaService;
|
|
610
|
+
if (!variables || !metaService) {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
const secret = await variables.get('SCENARIO_ACTOR_SECRET');
|
|
614
|
+
const apiUrl = await variables.get('API_URL');
|
|
615
|
+
if (!secret || !apiUrl) {
|
|
616
|
+
services?.logger?.warn('A scenario was started without actors but SCENARIO_ACTOR_SECRET / API_URL is not configured — running without actors.');
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
const actorsConfig = await metaService.getScenarioActorsMeta();
|
|
620
|
+
if (!actorsConfig || Object.keys(actorsConfig).length === 0) {
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
const signInPath = (await variables.get('SCENARIO_SIGN_IN_PATH')) ??
|
|
624
|
+
'/api/auth/sign-in/actor';
|
|
625
|
+
const rpcPath = (await variables.get('SCENARIO_RPC_PATH')) ?? '/rpc';
|
|
626
|
+
return createHttpScenarioActors({
|
|
627
|
+
apiUrl,
|
|
628
|
+
secret,
|
|
629
|
+
actors: actorsConfig,
|
|
630
|
+
signInPath,
|
|
631
|
+
rpcPath,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Start a new workflow run
|
|
636
|
+
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
637
|
+
* @param options.inline - If true, execute workflow directly without queue service
|
|
638
|
+
* @param options.startNode - Starting node ID for graph workflows (from wire config)
|
|
639
|
+
*/
|
|
604
640
|
/**
|
|
605
641
|
* Start a new workflow run
|
|
606
642
|
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
@@ -641,8 +677,12 @@ export class PikkuWorkflowService {
|
|
|
641
677
|
deterministic: workflowMeta.deterministic,
|
|
642
678
|
plannedSteps: workflowMeta.plannedSteps,
|
|
643
679
|
});
|
|
644
|
-
|
|
645
|
-
|
|
680
|
+
const actors = options?.actors ??
|
|
681
|
+
(workflowMeta.source === 'scenario'
|
|
682
|
+
? await this.resolveScenarioActors()
|
|
683
|
+
: undefined);
|
|
684
|
+
if (actors) {
|
|
685
|
+
this.runActors.set(runId, actors);
|
|
646
686
|
}
|
|
647
687
|
if (shouldInline) {
|
|
648
688
|
this.inlineRuns.add(runId);
|
|
@@ -236,9 +236,9 @@ export type WorkflowsMeta = Record<string, CommonWireMeta & {
|
|
|
236
236
|
context?: WorkflowContext;
|
|
237
237
|
dsl?: boolean;
|
|
238
238
|
expose?: boolean;
|
|
239
|
-
/** True for
|
|
240
|
-
|
|
241
|
-
/** Actor names a
|
|
239
|
+
/** True for pikkuScenario workflows (complex + actor steps). */
|
|
240
|
+
scenario?: boolean;
|
|
241
|
+
/** Actor names a scenario declares (personas it runs steps as). */
|
|
242
242
|
actors?: string[];
|
|
243
243
|
}>;
|
|
244
244
|
/**
|
|
@@ -251,13 +251,13 @@ export interface WorkflowRuntimeMeta {
|
|
|
251
251
|
name: string;
|
|
252
252
|
/** Pikku function name (for execution) */
|
|
253
253
|
pikkuFuncId: string;
|
|
254
|
-
/** Source type: 'dsl' (serializable), 'complex' (has inline steps), 'graph', '
|
|
255
|
-
source: 'dsl' | 'complex' | 'graph' | 'dynamic-workflow' | '
|
|
254
|
+
/** Source type: 'dsl' (serializable), 'complex' (has inline steps), 'graph', 'scenario' (complex + actor steps) */
|
|
255
|
+
source: 'dsl' | 'complex' | 'graph' | 'dynamic-workflow' | 'scenario';
|
|
256
256
|
/** Optional description */
|
|
257
257
|
description?: string;
|
|
258
258
|
/** Tags for organization */
|
|
259
259
|
tags?: string[];
|
|
260
|
-
/** Actor names a
|
|
260
|
+
/** Actor names a scenario declares (personas it runs steps as). */
|
|
261
261
|
actors?: string[];
|
|
262
262
|
/** Serialized nodes */
|
|
263
263
|
nodes?: Record<string, any>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.52",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"./workflow": "./dist/wirings/workflow/index.js",
|
|
25
25
|
"./workflow/timeline": "./dist/wirings/workflow/run-timeline.js",
|
|
26
26
|
"./workflow/types": "./dist/wirings/workflow/workflow.types.js",
|
|
27
|
+
"./actor-flow": "./dist/wirings/actor-flow/index.js",
|
|
27
28
|
"./channel/local": "./dist/wirings/channel/local/index.js",
|
|
28
29
|
"./channel/serverless": "./dist/wirings/channel/serverless/index.js",
|
|
29
30
|
"./http": "./dist/wirings/http/index.js",
|
package/run-tests.sh
CHANGED
|
@@ -44,16 +44,16 @@ if [ ${#files[@]} -eq 0 ]; then
|
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
46
|
# Construct the node command
|
|
47
|
-
node_cmd=
|
|
47
|
+
node_cmd=(node --import tsx --test)
|
|
48
48
|
|
|
49
49
|
# Append options based on flags
|
|
50
50
|
if [ "$watch_mode" = true ]; then
|
|
51
|
-
node_cmd
|
|
51
|
+
node_cmd+=(--watch)
|
|
52
52
|
fi
|
|
53
53
|
|
|
54
54
|
if [ "$coverage_mode" = true ]; then
|
|
55
|
-
node_cmd
|
|
55
|
+
node_cmd+=(--test-coverage-include="src/**/*.{ts,js}" --test-coverage-exclude="**/dist/**" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info)
|
|
56
56
|
fi
|
|
57
57
|
|
|
58
58
|
# Execute the node command with the expanded list of files
|
|
59
|
-
$node_cmd "${files[@]}"
|
|
59
|
+
"${node_cmd[@]}" "${files[@]}"
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,12 @@ export type {
|
|
|
33
33
|
PikkuWiringTypes,
|
|
34
34
|
PostgresConfig,
|
|
35
35
|
RequireAtLeastOne,
|
|
36
|
+
SecurityAuditIssue,
|
|
37
|
+
SecurityAuditReport,
|
|
38
|
+
SecurityAuditSummary,
|
|
39
|
+
SecurityAuditUpdate,
|
|
40
|
+
SecuritySeverity,
|
|
41
|
+
SecurityUpdateLevel,
|
|
36
42
|
SerializedError,
|
|
37
43
|
WireServices,
|
|
38
44
|
} from './types/core.types.js'
|