@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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,96 @@
|
|
|
1
|
+
## 0.12.52
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 61c9ce9: Add `actor.converse(...)` — actor agents for user journeys (#850)
|
|
6
|
+
|
|
7
|
+
An actor can now hold a dynamic, LLM-driven conversation with a target Pikku AI
|
|
8
|
+
agent in its own persona:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const verdict = await actors.pm.converse({
|
|
12
|
+
agent: 'todoBot',
|
|
13
|
+
task: 'Get a todo created for the launch',
|
|
14
|
+
evaluate: 'A todo about the launch now exists',
|
|
15
|
+
})
|
|
16
|
+
// verdict: { passed, reasoning, transcript }
|
|
17
|
+
// then assert deterministically as the same actor:
|
|
18
|
+
const todos = await actors.pm.invoke('listTodos', {})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The actor drives the target over the real transport (the agent's own
|
|
22
|
+
`agentRun` / `agentApprove` HTTP routes, signed in as the actor), plays the
|
|
23
|
+
persona from its `pikku.config.json` config, answers the agent's tool-approval
|
|
24
|
+
requests in-persona (`approvals: 'in-persona' | 'always' | 'never'`), and
|
|
25
|
+
returns its verdict on whether the task was met. Deterministic checks stay the
|
|
26
|
+
caller's job — they already hold the actor.
|
|
27
|
+
|
|
28
|
+
The conversation engine is transport-agnostic (persona LLM + injected target
|
|
29
|
+
driver); the persona's own turns run in-process via the configured
|
|
30
|
+
`aiAgentRunner` (`model` from the call or the actors-service default).
|
|
31
|
+
|
|
32
|
+
`agent` is typed against the generated agent-name union (`keyof AgentMap`), so
|
|
33
|
+
it's author-time checked and autocompleted in a typed project.
|
|
34
|
+
|
|
35
|
+
- f1f39f8: Bound the actor-flow approval loop (#850)
|
|
36
|
+
|
|
37
|
+
`converseWithTarget` now caps suspend→approve rounds within a single target turn
|
|
38
|
+
(default 16, override via `maxApprovalRounds`). A cooperative target completes
|
|
39
|
+
after a handful of rounds; a buggy or uncooperative one — e.g. re-requesting a
|
|
40
|
+
tool the persona keeps denying — previously could spin the inner loop forever
|
|
41
|
+
without ever spending a `maxTurns` credit. Exceeding the cap now throws instead
|
|
42
|
+
of hanging.
|
|
43
|
+
|
|
44
|
+
- c45e98d: Run user flows from the console, actors and all (#850)
|
|
45
|
+
|
|
46
|
+
Starting a `user-flow` workflow without explicit run actors (as the console's
|
|
47
|
+
Run button does) now auto-builds HTTP actors from `USER_FLOW_ACTOR_SECRET` and
|
|
48
|
+
`API_URL`: each actor signs in via the actor auth plugin — which mints the
|
|
49
|
+
`actor: true` user row on first sign-in — and drives its steps over HTTP as
|
|
50
|
+
that persona. When the secret or API base URL isn't configured the run simply
|
|
51
|
+
proceeds without actors (with a warning) instead of failing.
|
|
52
|
+
|
|
53
|
+
The workflow-detail view also gains the shared console header: the workflow
|
|
54
|
+
selector and the "complex workflow" note now live in the header bar, the right
|
|
55
|
+
details panel hides when it has nothing to show, and step nodes display their
|
|
56
|
+
DSL labels (e.g. `Double ${item}`).
|
|
57
|
+
|
|
58
|
+
- 472a349: Rename the userflow concept to scenario (#862). `pikkuUserFlow` becomes `pikkuScenario`, `pikku userflow run/list` becomes `pikku scenario run/list`, the workflow meta flag `userFlow` becomes `scenario`, actor types are now `ScenarioActor`/`ScenarioActors`/`ScenarioActorConfig` (`createHttpScenarioActors`), pikku.config.json's `userFlows` key becomes `scenarios`, the generated actors file is `pikku-scenario-actors.gen.ts` (`createScenarioActors`), the actor sign-in secret env var is `SCENARIO_ACTOR_SECRET`, and the console's User Flows view is now Scenarios.
|
|
59
|
+
|
|
60
|
+
## 0.12.51
|
|
61
|
+
|
|
62
|
+
### Patch Changes
|
|
63
|
+
|
|
64
|
+
- 7ebea62: Tree-shake addon registrations in filtered inspector states (per-unit deploy codegen).
|
|
65
|
+
- `filterInspectorState` drops an addon's `wireAddonDeclarations`/`usedAddons` unless something kept actually references it (kept wiring targeting `namespace:*`, kept agent/MCP tool, or a body-level `rpc.invoke('namespace:*')` from a file that still contains a kept function). The generated per-unit bootstrap no longer imports unused addon package bootstraps — previously every deploy unit registered every addon's entire function surface, which pulled dev-only code (e.g. `@pikku/addon-console`'s static `node:fs` imports) into Cloudflare Worker bundles and failed upload with `No such module "node:fs"`.
|
|
66
|
+
- Body-level `rpc.invoke()` targets are now tracked per source file (`rpc.invokedFunctionsByFile`) so wiring-level `ref()` targets no longer pin an addon into every unit.
|
|
67
|
+
- `aggregateRequiredServices` computes addon parent services per used addon function (from the addon's shipped per-function `services` meta) instead of blanket-adding `addonRequiredParentServices` — and matches namespaced ids only, so bare project function names colliding with addon function names no longer force the blanket.
|
|
68
|
+
- Addon builds keep per-function `services` in the shipped `pikku-functions-meta.gen.json` so parent projects can do the above; addons built before this fall back to the blanket.
|
|
69
|
+
- HTTP route meta records `refTarget` for `ref('namespace:fn')`-wired routes, so per-unit filtering keeps the addon registration (and only that function's services) when the route deploys.
|
|
70
|
+
|
|
71
|
+
- e57dd65: feat(console): surface the `pikku audit` report in the dev console
|
|
72
|
+
|
|
73
|
+
Adds a view-only **Security** screen to the pikku dev console that renders the
|
|
74
|
+
dependency audit produced by `pikku audit` (`.pikku/audit.json`): known
|
|
75
|
+
vulnerabilities (severity, advisory, recommended version) and available
|
|
76
|
+
dependency updates.
|
|
77
|
+
- `@pikku/core`: exports the canonical `SecurityAuditReport` artifact type (plus
|
|
78
|
+
`SecurityAuditIssue`/`SecurityAuditUpdate`/`SecurityAuditSummary` and the
|
|
79
|
+
`SecuritySeverity`/`SecurityUpdateLevel` unions) — a single source of truth
|
|
80
|
+
shared by the CLI (writer), the console addon (reader) and the console UI.
|
|
81
|
+
- `@pikku/addon-console`: `getSecurityAudit` reads the audit artifact via the
|
|
82
|
+
meta service; `runSecurityAudit` triggers `pikku audit --outdated` server-side
|
|
83
|
+
(regenerating the artifact) — same shape as the Run Tests action;
|
|
84
|
+
`updateDependency` bumps a package in `package.json` (preserving the `^`/`~`
|
|
85
|
+
range), runs `bun install`, re-audits, and returns the fresh report.
|
|
86
|
+
- `@pikku/console`: new `SecurityPage` with a **Run audit** button + reusable
|
|
87
|
+
presentational `SecurityAuditView` (exported, so downstream consoles can wrap
|
|
88
|
+
it with their own actions) + `useSecurityAudit`/`useRunSecurityAudit`/
|
|
89
|
+
`useUpdateDependency` hooks. Issues/Dependencies lenses; per-finding
|
|
90
|
+
remediation slot right-aligned in the row header (`renderRemediation`,
|
|
91
|
+
defaulting to the OSS `UpdateDependencyButton`; Fabric swaps in its own
|
|
92
|
+
sandbox-verified action). Empty state until an audit has been run.
|
|
93
|
+
|
|
1
94
|
## 0.12.50
|
|
2
95
|
|
|
3
96
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module @pikku/core
|
|
3
3
|
*/
|
|
4
|
-
export type { CommonWireMeta, CoreConfig, CorePikkuMiddleware, CorePikkuMiddlewareConfig, CorePikkuMiddlewareFactory, CorePikkuMiddlewareGroup, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, ServerLifecycle, FunctionMeta, FunctionRuntimeMeta, FunctionServicesMeta, FunctionWiresMeta, FunctionsMeta, FunctionsRuntimeMeta, JSONPrimitive, JSONValue, MakeRequired, MiddlewareMetadata, MiddlewarePriority, PermissionMetadata, PickOptional, PickRequired, PikkuAIMiddlewareHooks, PikkuWire, PikkuRawWire, PikkuWiringTypes, PostgresConfig, RequireAtLeastOne, SerializedError, WireServices, } from './types/core.types.js';
|
|
4
|
+
export type { CommonWireMeta, CoreConfig, CorePikkuMiddleware, CorePikkuMiddlewareConfig, CorePikkuMiddlewareFactory, CorePikkuMiddlewareGroup, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, ServerLifecycle, FunctionMeta, FunctionRuntimeMeta, FunctionServicesMeta, FunctionWiresMeta, FunctionsMeta, FunctionsRuntimeMeta, JSONPrimitive, JSONValue, MakeRequired, MiddlewareMetadata, MiddlewarePriority, PermissionMetadata, PickOptional, PickRequired, PikkuAIMiddlewareHooks, PikkuWire, PikkuRawWire, PikkuWiringTypes, PostgresConfig, RequireAtLeastOne, SecurityAuditIssue, SecurityAuditReport, SecurityAuditSummary, SecurityAuditUpdate, SecuritySeverity, SecurityUpdateLevel, SerializedError, WireServices, } from './types/core.types.js';
|
|
5
5
|
export { pikkuAIMiddleware, pikkuChannelMiddleware, pikkuChannelMiddlewareFactory, pikkuMiddleware, pikkuMiddlewareFactory, } from './types/core.types.js';
|
|
6
6
|
export type { CorePikkuAuth, CorePikkuAuthConfig, CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuPermission, CorePikkuPermissionConfig, CorePikkuPermissionFactory, CorePikkuApprovalDescription, CorePermissionGroup, ZodLike, } from './function/functions.types.js';
|
|
7
7
|
export { pikkuAuth, pikkuPermission, pikkuPermissionFactory, pikkuApprovalDescription, } from './function/functions.types.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ScenarioActor, ScenarioActorConfig, ScenarioActors } from './scenario-actors-service.js';
|
|
2
|
+
import type { ConverseOptions, ActorFlowVerdict } from '../wirings/actor-flow/actor-flow.types.js';
|
|
3
|
+
export interface HttpScenarioActorsConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Base API URL of the target app, INCLUDING the HTTP prefix — e.g.
|
|
6
|
+
* `https://app.example.com/api` or `http://localhost:4000/api`. Actor
|
|
7
|
+
* sign-in is reached at `${apiUrl}${signInPath}` and exposed RPCs at
|
|
8
|
+
* `${apiUrl}${rpcPath}/:rpcName`.
|
|
9
|
+
*/
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
/**
|
|
12
|
+
* The actor impersonation secret. Sign-in only ever works for user rows
|
|
13
|
+
* flagged `actor: true` — knowing the secret never impersonates real users.
|
|
14
|
+
*/
|
|
15
|
+
secret: string;
|
|
16
|
+
/** Actor name → config (usually from pikku.config.json's actor registry). */
|
|
17
|
+
actors: Record<string, ScenarioActorConfig>;
|
|
18
|
+
/** Sign-in path under apiUrl. Default: the actor plugin's `/auth/sign-in/actor`. */
|
|
19
|
+
signInPath?: string;
|
|
20
|
+
/** Exposed-RPC path prefix under apiUrl. Default `/rpc`. */
|
|
21
|
+
rpcPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Default model the persona uses when `actor.converse(...)` is called without
|
|
24
|
+
* an explicit `model`. The persona's own turns/approvals/evaluation run
|
|
25
|
+
* in-process via the configured `aiAgentRunner`.
|
|
26
|
+
*/
|
|
27
|
+
model?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Default HTTP-backed actor. Signs in lazily on first invoke via the Better
|
|
31
|
+
* Auth actor plugin (`POST /auth/sign-in/actor` with `{ email, secret }` —
|
|
32
|
+
* the plugin upserts the actor-flagged user row and mints a session whose
|
|
33
|
+
* `actor` flag flows into audits/analytics). Holds the session cookies for
|
|
34
|
+
* its lifetime; a 401 mid-run re-logs-in once (long health-check runs can
|
|
35
|
+
* outlive a session).
|
|
36
|
+
*/
|
|
37
|
+
export declare class HttpScenarioActor implements ScenarioActor {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
private actorConfig;
|
|
40
|
+
private config;
|
|
41
|
+
private cookie;
|
|
42
|
+
private origin;
|
|
43
|
+
constructor(name: string, actorConfig: ScenarioActorConfig, config: HttpScenarioActorsConfig);
|
|
44
|
+
get email(): string;
|
|
45
|
+
invoke(rpcName: string, data: unknown): Promise<unknown>;
|
|
46
|
+
converse(options: ConverseOptions): Promise<ActorFlowVerdict>;
|
|
47
|
+
/** Start/continue the target agent's run over HTTP as this actor. */
|
|
48
|
+
private agentRun;
|
|
49
|
+
/** Answer the target agent's pending approvals over HTTP and continue. */
|
|
50
|
+
private agentApprove;
|
|
51
|
+
/**
|
|
52
|
+
* POST an agent HTTP route (raw body, not RPC-wrapped). Signs in lazily: the
|
|
53
|
+
* first call goes out with whatever cookie we hold (none, for a no-auth
|
|
54
|
+
* agent), and only a 401 triggers `login()` + one retry. This lets an actor
|
|
55
|
+
* converse with a no-auth agent without any sign-in wiring, while still
|
|
56
|
+
* authenticating against agents that require a session.
|
|
57
|
+
*/
|
|
58
|
+
private postAgent;
|
|
59
|
+
private postRpc;
|
|
60
|
+
private readRpcResponse;
|
|
61
|
+
private login;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build the injected `actors` service from the config registry: actor name →
|
|
65
|
+
* lazy HTTP actor. Wire the result as the `actors` singleton service.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createHttpScenarioActors(config: HttpScenarioActorsConfig): ScenarioActors;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { runConversation } from '../wirings/actor-flow/run-conversation.js';
|
|
2
|
+
import { getSingletonServices } from '../pikku-state.js';
|
|
3
|
+
import { AIProviderNotConfiguredError } from '../errors/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default HTTP-backed actor. Signs in lazily on first invoke via the Better
|
|
6
|
+
* Auth actor plugin (`POST /auth/sign-in/actor` with `{ email, secret }` —
|
|
7
|
+
* the plugin upserts the actor-flagged user row and mints a session whose
|
|
8
|
+
* `actor` flag flows into audits/analytics). Holds the session cookies for
|
|
9
|
+
* its lifetime; a 401 mid-run re-logs-in once (long health-check runs can
|
|
10
|
+
* outlive a session).
|
|
11
|
+
*/
|
|
12
|
+
export class HttpScenarioActor {
|
|
13
|
+
name;
|
|
14
|
+
actorConfig;
|
|
15
|
+
config;
|
|
16
|
+
cookie = null;
|
|
17
|
+
origin;
|
|
18
|
+
constructor(name, actorConfig, config) {
|
|
19
|
+
this.name = name;
|
|
20
|
+
this.actorConfig = actorConfig;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.origin = new URL(config.apiUrl).origin;
|
|
23
|
+
}
|
|
24
|
+
get email() {
|
|
25
|
+
return this.actorConfig.email;
|
|
26
|
+
}
|
|
27
|
+
async invoke(rpcName, data) {
|
|
28
|
+
const cookie = this.cookie ?? (await this.login());
|
|
29
|
+
const res = await this.postRpc(rpcName, data, cookie);
|
|
30
|
+
if (res.status === 401) {
|
|
31
|
+
// Session expired mid-run — re-login once and retry.
|
|
32
|
+
this.cookie = null;
|
|
33
|
+
return this.readRpcResponse(rpcName, await this.postRpc(rpcName, data, await this.login()));
|
|
34
|
+
}
|
|
35
|
+
return this.readRpcResponse(rpcName, res);
|
|
36
|
+
}
|
|
37
|
+
async converse(options) {
|
|
38
|
+
const { aiAgentRunner } = getSingletonServices();
|
|
39
|
+
if (!aiAgentRunner) {
|
|
40
|
+
throw new AIProviderNotConfiguredError();
|
|
41
|
+
}
|
|
42
|
+
const model = options.model ?? this.config.model;
|
|
43
|
+
if (!model) {
|
|
44
|
+
throw new Error(`[scenario] actor '${this.name}' converse needs a model — pass options.model or set 'model' on the actors service`);
|
|
45
|
+
}
|
|
46
|
+
const threadId = globalThis.crypto.randomUUID();
|
|
47
|
+
const resourceId = `actor:${this.name}`;
|
|
48
|
+
return runConversation({
|
|
49
|
+
persona: this.actorConfig,
|
|
50
|
+
personaName: this.actorConfig.name ?? this.name,
|
|
51
|
+
agentName: options.agent,
|
|
52
|
+
task: options.task,
|
|
53
|
+
evaluate: options.evaluate,
|
|
54
|
+
approvals: options.approvals,
|
|
55
|
+
model,
|
|
56
|
+
maxTurns: options.maxTurns,
|
|
57
|
+
llm: (params) => aiAgentRunner.run(params),
|
|
58
|
+
target: {
|
|
59
|
+
run: (message) => this.agentRun(options.agent, message, threadId, resourceId),
|
|
60
|
+
approve: (runId, decisions) => this.agentApprove(options.agent, runId, decisions),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Start/continue the target agent's run over HTTP as this actor. */
|
|
65
|
+
async agentRun(agentName, message, threadId, resourceId) {
|
|
66
|
+
const raw = await this.postAgent(`agent/${agentName}`, {
|
|
67
|
+
message,
|
|
68
|
+
threadId,
|
|
69
|
+
resourceId,
|
|
70
|
+
});
|
|
71
|
+
return normalizeAgentReply(raw);
|
|
72
|
+
}
|
|
73
|
+
/** Answer the target agent's pending approvals over HTTP and continue. */
|
|
74
|
+
async agentApprove(agentName, runId, decisions) {
|
|
75
|
+
const raw = await this.postAgent(`agent/${agentName}/approve`, {
|
|
76
|
+
runId,
|
|
77
|
+
approvals: decisions,
|
|
78
|
+
});
|
|
79
|
+
return normalizeAgentReply(raw);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* POST an agent HTTP route (raw body, not RPC-wrapped). Signs in lazily: the
|
|
83
|
+
* first call goes out with whatever cookie we hold (none, for a no-auth
|
|
84
|
+
* agent), and only a 401 triggers `login()` + one retry. This lets an actor
|
|
85
|
+
* converse with a no-auth agent without any sign-in wiring, while still
|
|
86
|
+
* authenticating against agents that require a session.
|
|
87
|
+
*/
|
|
88
|
+
async postAgent(subPath, body) {
|
|
89
|
+
const rpcPath = this.config.rpcPath ?? '/rpc';
|
|
90
|
+
const url = `${this.config.apiUrl}${rpcPath}/${subPath}`;
|
|
91
|
+
const send = (cookie) => fetch(url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'content-type': 'application/json',
|
|
95
|
+
origin: this.origin,
|
|
96
|
+
...(cookie ? { cookie } : {}),
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
});
|
|
100
|
+
let res = await send(this.cookie);
|
|
101
|
+
if (res.status === 401) {
|
|
102
|
+
this.cookie = null;
|
|
103
|
+
res = await send(await this.login());
|
|
104
|
+
}
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const text = (await res.text().catch(() => '')).slice(0, 300);
|
|
107
|
+
throw new Error(`[scenario] agent call '${subPath}' as '${this.name}' returned ${res.status}: ${text}`);
|
|
108
|
+
}
|
|
109
|
+
if (res.status === 204)
|
|
110
|
+
return undefined;
|
|
111
|
+
const text = await res.text();
|
|
112
|
+
return text ? JSON.parse(text) : undefined;
|
|
113
|
+
}
|
|
114
|
+
async postRpc(rpcName, data, cookie) {
|
|
115
|
+
const rpcPath = this.config.rpcPath ?? '/rpc';
|
|
116
|
+
return fetch(`${this.config.apiUrl}${rpcPath}/${rpcName}`, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: {
|
|
119
|
+
'content-type': 'application/json',
|
|
120
|
+
origin: this.origin,
|
|
121
|
+
cookie,
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({ data }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async readRpcResponse(rpcName, res) {
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
const body = (await res.text().catch(() => '')).slice(0, 300);
|
|
129
|
+
throw new Error(`[scenario] '${rpcName}' as '${this.name}' returned ${res.status}: ${body}`);
|
|
130
|
+
}
|
|
131
|
+
if (res.status === 204)
|
|
132
|
+
return undefined;
|
|
133
|
+
const text = await res.text();
|
|
134
|
+
return text ? JSON.parse(text) : undefined;
|
|
135
|
+
}
|
|
136
|
+
async login() {
|
|
137
|
+
const signInPath = this.config.signInPath ?? '/auth/sign-in/actor';
|
|
138
|
+
const res = await fetch(`${this.config.apiUrl}${signInPath}`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'content-type': 'application/json', origin: this.origin },
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
email: this.actorConfig.email,
|
|
143
|
+
name: this.actorConfig.name ?? this.name,
|
|
144
|
+
secret: this.config.secret,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
const body = (await res.text().catch(() => '')).slice(0, 300);
|
|
149
|
+
throw new Error(`[scenario] actor sign-in failed for '${this.name}' (${res.status}): ${body}`);
|
|
150
|
+
}
|
|
151
|
+
const setCookies = res.headers.getSetCookie?.() ?? [];
|
|
152
|
+
const cookie = setCookies
|
|
153
|
+
.map((c) => c.split(';')[0])
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.join('; ');
|
|
156
|
+
if (!cookie) {
|
|
157
|
+
throw new Error(`[scenario] actor sign-in for '${this.name}' returned no session cookie`);
|
|
158
|
+
}
|
|
159
|
+
this.cookie = cookie;
|
|
160
|
+
return cookie;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Normalize an agentRun/agentApprove HTTP response into a TargetAgentReply. */
|
|
164
|
+
function normalizeAgentReply(raw) {
|
|
165
|
+
const r = (raw ?? {});
|
|
166
|
+
const pending = Array.isArray(r.pendingApprovals)
|
|
167
|
+
? r.pendingApprovals.map((p) => ({
|
|
168
|
+
toolCallId: String(p.toolCallId),
|
|
169
|
+
toolName: String(p.toolName),
|
|
170
|
+
args: p.args,
|
|
171
|
+
reason: typeof p.reason === 'string' ? p.reason : undefined,
|
|
172
|
+
}))
|
|
173
|
+
: undefined;
|
|
174
|
+
return {
|
|
175
|
+
text: typeof r.text === 'string' ? r.text : '',
|
|
176
|
+
runId: typeof r.runId === 'string' ? r.runId : '',
|
|
177
|
+
status: r.status === 'completed' || r.status === 'suspended'
|
|
178
|
+
? r.status
|
|
179
|
+
: undefined,
|
|
180
|
+
pendingApprovals: pending,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Build the injected `actors` service from the config registry: actor name →
|
|
185
|
+
* lazy HTTP actor. Wire the result as the `actors` singleton service.
|
|
186
|
+
*/
|
|
187
|
+
export function createHttpScenarioActors(config) {
|
|
188
|
+
const actors = {};
|
|
189
|
+
for (const [name, actorConfig] of Object.entries(config.actors)) {
|
|
190
|
+
actors[name] = new HttpScenarioActor(name, actorConfig, config);
|
|
191
|
+
}
|
|
192
|
+
return actors;
|
|
193
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UserFlowActor, UserFlowActorConfig, UserFlowActors } from './user-flow-actors-service.js';
|
|
2
|
+
import type { ConverseOptions, ActorFlowVerdict } from '../wirings/actor-flow/actor-flow.types.js';
|
|
2
3
|
export interface HttpUserFlowActorsConfig {
|
|
3
4
|
/**
|
|
4
5
|
* Base API URL of the target app, INCLUDING the HTTP prefix — e.g.
|
|
@@ -18,6 +19,12 @@ export interface HttpUserFlowActorsConfig {
|
|
|
18
19
|
signInPath?: string;
|
|
19
20
|
/** Exposed-RPC path prefix under apiUrl. Default `/rpc`. */
|
|
20
21
|
rpcPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Default model the persona uses when `actor.converse(...)` is called without
|
|
24
|
+
* an explicit `model`. The persona's own turns/approvals/evaluation run
|
|
25
|
+
* in-process via the configured `aiAgentRunner`.
|
|
26
|
+
*/
|
|
27
|
+
model?: string;
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
30
|
* Default HTTP-backed actor. Signs in lazily on first invoke via the Better
|
|
@@ -36,6 +43,19 @@ export declare class HttpUserFlowActor implements UserFlowActor {
|
|
|
36
43
|
constructor(name: string, actorConfig: UserFlowActorConfig, config: HttpUserFlowActorsConfig);
|
|
37
44
|
get email(): string;
|
|
38
45
|
invoke(rpcName: string, data: unknown): Promise<unknown>;
|
|
46
|
+
converse(options: ConverseOptions): Promise<ActorFlowVerdict>;
|
|
47
|
+
/** Start/continue the target agent's run over HTTP as this actor. */
|
|
48
|
+
private agentRun;
|
|
49
|
+
/** Answer the target agent's pending approvals over HTTP and continue. */
|
|
50
|
+
private agentApprove;
|
|
51
|
+
/**
|
|
52
|
+
* POST an agent HTTP route (raw body, not RPC-wrapped). Signs in lazily: the
|
|
53
|
+
* first call goes out with whatever cookie we hold (none, for a no-auth
|
|
54
|
+
* agent), and only a 401 triggers `login()` + one retry. This lets an actor
|
|
55
|
+
* converse with a no-auth agent without any sign-in wiring, while still
|
|
56
|
+
* authenticating against agents that require a session.
|
|
57
|
+
*/
|
|
58
|
+
private postAgent;
|
|
39
59
|
private postRpc;
|
|
40
60
|
private readRpcResponse;
|
|
41
61
|
private login;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { runConversation } from '../wirings/actor-flow/run-conversation.js';
|
|
2
|
+
import { getSingletonServices } from '../pikku-state.js';
|
|
3
|
+
import { AIProviderNotConfiguredError } from '../errors/errors.js';
|
|
1
4
|
/**
|
|
2
5
|
* Default HTTP-backed actor. Signs in lazily on first invoke via the Better
|
|
3
6
|
* Auth actor plugin (`POST /auth/sign-in/actor` with `{ email, secret }` —
|
|
@@ -31,6 +34,83 @@ export class HttpUserFlowActor {
|
|
|
31
34
|
}
|
|
32
35
|
return this.readRpcResponse(rpcName, res);
|
|
33
36
|
}
|
|
37
|
+
async converse(options) {
|
|
38
|
+
const { aiAgentRunner } = getSingletonServices();
|
|
39
|
+
if (!aiAgentRunner) {
|
|
40
|
+
throw new AIProviderNotConfiguredError();
|
|
41
|
+
}
|
|
42
|
+
const model = options.model ?? this.config.model;
|
|
43
|
+
if (!model) {
|
|
44
|
+
throw new Error(`[user-flow] actor '${this.name}' converse needs a model — pass options.model or set 'model' on the actors service`);
|
|
45
|
+
}
|
|
46
|
+
const threadId = globalThis.crypto.randomUUID();
|
|
47
|
+
const resourceId = `actor:${this.name}`;
|
|
48
|
+
return runConversation({
|
|
49
|
+
persona: this.actorConfig,
|
|
50
|
+
personaName: this.actorConfig.name ?? this.name,
|
|
51
|
+
agentName: options.agent,
|
|
52
|
+
task: options.task,
|
|
53
|
+
evaluate: options.evaluate,
|
|
54
|
+
approvals: options.approvals,
|
|
55
|
+
model,
|
|
56
|
+
maxTurns: options.maxTurns,
|
|
57
|
+
llm: (params) => aiAgentRunner.run(params),
|
|
58
|
+
target: {
|
|
59
|
+
run: (message) => this.agentRun(options.agent, message, threadId, resourceId),
|
|
60
|
+
approve: (runId, decisions) => this.agentApprove(options.agent, runId, decisions),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Start/continue the target agent's run over HTTP as this actor. */
|
|
65
|
+
async agentRun(agentName, message, threadId, resourceId) {
|
|
66
|
+
const raw = await this.postAgent(`agent/${agentName}`, {
|
|
67
|
+
message,
|
|
68
|
+
threadId,
|
|
69
|
+
resourceId,
|
|
70
|
+
});
|
|
71
|
+
return normalizeAgentReply(raw);
|
|
72
|
+
}
|
|
73
|
+
/** Answer the target agent's pending approvals over HTTP and continue. */
|
|
74
|
+
async agentApprove(agentName, runId, decisions) {
|
|
75
|
+
const raw = await this.postAgent(`agent/${agentName}/approve`, {
|
|
76
|
+
runId,
|
|
77
|
+
approvals: decisions,
|
|
78
|
+
});
|
|
79
|
+
return normalizeAgentReply(raw);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* POST an agent HTTP route (raw body, not RPC-wrapped). Signs in lazily: the
|
|
83
|
+
* first call goes out with whatever cookie we hold (none, for a no-auth
|
|
84
|
+
* agent), and only a 401 triggers `login()` + one retry. This lets an actor
|
|
85
|
+
* converse with a no-auth agent without any sign-in wiring, while still
|
|
86
|
+
* authenticating against agents that require a session.
|
|
87
|
+
*/
|
|
88
|
+
async postAgent(subPath, body) {
|
|
89
|
+
const rpcPath = this.config.rpcPath ?? '/rpc';
|
|
90
|
+
const url = `${this.config.apiUrl}${rpcPath}/${subPath}`;
|
|
91
|
+
const send = (cookie) => fetch(url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'content-type': 'application/json',
|
|
95
|
+
origin: this.origin,
|
|
96
|
+
...(cookie ? { cookie } : {}),
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
});
|
|
100
|
+
let res = await send(this.cookie);
|
|
101
|
+
if (res.status === 401) {
|
|
102
|
+
this.cookie = null;
|
|
103
|
+
res = await send(await this.login());
|
|
104
|
+
}
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const text = (await res.text().catch(() => '')).slice(0, 300);
|
|
107
|
+
throw new Error(`[user-flow] agent call '${subPath}' as '${this.name}' returned ${res.status}: ${text}`);
|
|
108
|
+
}
|
|
109
|
+
if (res.status === 204)
|
|
110
|
+
return undefined;
|
|
111
|
+
const text = await res.text();
|
|
112
|
+
return text ? JSON.parse(text) : undefined;
|
|
113
|
+
}
|
|
34
114
|
async postRpc(rpcName, data, cookie) {
|
|
35
115
|
const rpcPath = this.config.rpcPath ?? '/rpc';
|
|
36
116
|
return fetch(`${this.config.apiUrl}${rpcPath}/${rpcName}`, {
|
|
@@ -80,6 +160,26 @@ export class HttpUserFlowActor {
|
|
|
80
160
|
return cookie;
|
|
81
161
|
}
|
|
82
162
|
}
|
|
163
|
+
/** Normalize an agentRun/agentApprove HTTP response into a TargetAgentReply. */
|
|
164
|
+
function normalizeAgentReply(raw) {
|
|
165
|
+
const r = (raw ?? {});
|
|
166
|
+
const pending = Array.isArray(r.pendingApprovals)
|
|
167
|
+
? r.pendingApprovals.map((p) => ({
|
|
168
|
+
toolCallId: String(p.toolCallId),
|
|
169
|
+
toolName: String(p.toolName),
|
|
170
|
+
args: p.args,
|
|
171
|
+
reason: typeof p.reason === 'string' ? p.reason : undefined,
|
|
172
|
+
}))
|
|
173
|
+
: undefined;
|
|
174
|
+
return {
|
|
175
|
+
text: typeof r.text === 'string' ? r.text : '',
|
|
176
|
+
runId: typeof r.runId === 'string' ? r.runId : '',
|
|
177
|
+
status: r.status === 'completed' || r.status === 'suspended'
|
|
178
|
+
? r.status
|
|
179
|
+
: undefined,
|
|
180
|
+
pendingApprovals: pending,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
83
183
|
/**
|
|
84
184
|
* Build the injected `actors` service from the config registry: actor name →
|
|
85
185
|
* lazy HTTP actor. Wire the result as the `actors` singleton service.
|
package/dist/services/index.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
|
|
18
18
|
export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
|
|
19
19
|
export { LocalGatewayService } from './local-gateway-service.js';
|
|
20
20
|
export type { ContentService, SignContentKeyArgs, SignURLArgs, GetUploadURLArgs, UploadURLResult, BucketKeyArgs, WriteFileArgs, CopyFileArgs, } from './content-service.js';
|
|
21
|
-
export type {
|
|
22
|
-
export {
|
|
21
|
+
export type { ScenarioActor, ScenarioActorConfig, ScenarioActors, } from './scenario-actors-service.js';
|
|
22
|
+
export { HttpScenarioActor, createHttpScenarioActors, type HttpScenarioActorsConfig, } from './http-scenario-actors.js';
|
|
23
23
|
export type { JWTService } from './jwt-service.js';
|
|
24
24
|
export type { EmailService, EmailTemplateReference, SendEmailInput, SendEmailResult, SendHTMLEmailInput, SendTemplateEmailInput, SendTextEmailInput, } from './email-service.js';
|
|
25
25
|
export type { Logger } from './logger.js';
|
package/dist/services/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { InMemoryQueueService } from './in-memory-queue-service.js';
|
|
|
17
17
|
export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
|
18
18
|
export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
|
|
19
19
|
export { LocalGatewayService } from './local-gateway-service.js';
|
|
20
|
-
export {
|
|
20
|
+
export { HttpScenarioActor, createHttpScenarioActors, } from './http-scenario-actors.js';
|
|
21
21
|
export { TypedCredentialService } from './typed-credential-service.js';
|
|
22
22
|
export { NoopAuditService, createInvocationAudit, resolveAuditActorFromWire, resolveAuditConfig, } from './audit-service.js';
|
|
23
23
|
export { InMemorySessionStore } from './in-memory-session-store.js';
|
|
@@ -6,7 +6,7 @@ import type { QueueWorkersMeta } from '../wirings/queue/queue.types.js';
|
|
|
6
6
|
import type { CLIMeta } from '../wirings/cli/cli.types.js';
|
|
7
7
|
import type { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '../wirings/mcp/mcp.types.js';
|
|
8
8
|
import type { WorkflowsMeta } from '../wirings/workflow/workflow.types.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { ScenarioActorConfig } from './scenario-actors-service.js';
|
|
10
10
|
import type { TriggerMeta, TriggerSourceMeta } from '../wirings/trigger/trigger.types.js';
|
|
11
11
|
import type { SecretDefinitionsMeta } from '../wirings/secret/secret.types.js';
|
|
12
12
|
import type { CredentialDefinitionsMeta } from '../wirings/credential/credential.types.js';
|
|
@@ -141,7 +141,7 @@ export interface MetaService {
|
|
|
141
141
|
getGatewayMeta(): Promise<GatewaysMeta>;
|
|
142
142
|
getRpcMeta(): Promise<RPCMetaRecord>;
|
|
143
143
|
getWorkflowMeta(): Promise<WorkflowsMeta>;
|
|
144
|
-
|
|
144
|
+
getScenarioActorsMeta(): Promise<Record<string, ScenarioActorConfig>>;
|
|
145
145
|
getTriggerMeta(): Promise<TriggerMeta>;
|
|
146
146
|
getTriggerSourceMeta(): Promise<TriggerSourceMeta>;
|
|
147
147
|
getFunctionsMeta(): Promise<FunctionsMeta>;
|
|
@@ -173,7 +173,7 @@ export declare class LocalMetaService implements MetaService {
|
|
|
173
173
|
private gatewayMetaCache;
|
|
174
174
|
private rpcMetaCache;
|
|
175
175
|
private workflowMetaCache;
|
|
176
|
-
private
|
|
176
|
+
private scenarioActorsMetaCache;
|
|
177
177
|
private triggerMetaCache;
|
|
178
178
|
private triggerSourceMetaCache;
|
|
179
179
|
private functionsMetaCache;
|
|
@@ -200,7 +200,7 @@ export declare class LocalMetaService implements MetaService {
|
|
|
200
200
|
getGatewayMeta(): Promise<GatewaysMeta>;
|
|
201
201
|
getRpcMeta(): Promise<RPCMetaRecord>;
|
|
202
202
|
getWorkflowMeta(): Promise<WorkflowsMeta>;
|
|
203
|
-
|
|
203
|
+
getScenarioActorsMeta(): Promise<Record<string, ScenarioActorConfig>>;
|
|
204
204
|
getTriggerMeta(): Promise<TriggerMeta>;
|
|
205
205
|
getTriggerSourceMeta(): Promise<TriggerSourceMeta>;
|
|
206
206
|
getFunctionsMeta(): Promise<FunctionsMeta>;
|
|
@@ -15,7 +15,7 @@ export class LocalMetaService {
|
|
|
15
15
|
gatewayMetaCache = null;
|
|
16
16
|
rpcMetaCache = null;
|
|
17
17
|
workflowMetaCache = null;
|
|
18
|
-
|
|
18
|
+
scenarioActorsMetaCache = null;
|
|
19
19
|
triggerMetaCache = null;
|
|
20
20
|
triggerSourceMetaCache = null;
|
|
21
21
|
functionsMetaCache = null;
|
|
@@ -64,7 +64,7 @@ export class LocalMetaService {
|
|
|
64
64
|
this.gatewayMetaCache = null;
|
|
65
65
|
this.rpcMetaCache = null;
|
|
66
66
|
this.workflowMetaCache = null;
|
|
67
|
-
this.
|
|
67
|
+
this.scenarioActorsMetaCache = null;
|
|
68
68
|
this.triggerMetaCache = null;
|
|
69
69
|
this.triggerSourceMetaCache = null;
|
|
70
70
|
this.functionsMetaCache = null;
|
|
@@ -203,12 +203,12 @@ export class LocalMetaService {
|
|
|
203
203
|
return this.workflowMetaCache;
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
-
async
|
|
207
|
-
if (this.
|
|
208
|
-
return this.
|
|
209
|
-
const content = await this.readFile('workflow/
|
|
210
|
-
this.
|
|
211
|
-
return this.
|
|
206
|
+
async getScenarioActorsMeta() {
|
|
207
|
+
if (this.scenarioActorsMetaCache)
|
|
208
|
+
return this.scenarioActorsMetaCache;
|
|
209
|
+
const content = await this.readFile('workflow/scenario-actors.gen.json');
|
|
210
|
+
this.scenarioActorsMetaCache = content ? JSON.parse(content) : {};
|
|
211
|
+
return this.scenarioActorsMetaCache;
|
|
212
212
|
}
|
|
213
213
|
async getTriggerMeta() {
|
|
214
214
|
if (this.triggerMetaCache)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ConverseOptions, ActorFlowVerdict } from '../wirings/actor-flow/actor-flow.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A scenario actor: a synthetic user (a normal user row flagged `actor`) that
|
|
4
|
+
* workflow steps can run as. Passed to `workflow.do(step, rpc, data, { actor })`
|
|
5
|
+
* — the step then goes through the actor's authenticated client over the REAL
|
|
6
|
+
* transport (auth middleware, permissions, serialization all exercised),
|
|
7
|
+
* never through internal dispatch. Login is lazy: the first `invoke` signs the
|
|
8
|
+
* actor in and the session is cached for the actor's lifetime.
|
|
9
|
+
*/
|
|
10
|
+
export interface ScenarioActor<TAgentName extends string = string> {
|
|
11
|
+
/** Stable actor name (the key in pikku.config.json's actor registry). */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** The actor's user email — flows use it for invites/lookups. */
|
|
14
|
+
readonly email: string;
|
|
15
|
+
/** Invoke an exposed RPC as this actor over the real transport. */
|
|
16
|
+
invoke(rpcName: string, data: unknown): Promise<unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Hold a dynamic conversation with a target Pikku AI agent, in THIS actor's
|
|
19
|
+
* persona (personality/jobTitle). Drives the target over the real transport
|
|
20
|
+
* as the signed-in actor, answers its tool-approval requests in-persona, and
|
|
21
|
+
* returns the actor's verdict on whether the task was met. Deterministic
|
|
22
|
+
* checks are the caller's job — use `invoke` afterwards. In a typed project
|
|
23
|
+
* `agent` is constrained to the generated union of agent names.
|
|
24
|
+
*/
|
|
25
|
+
converse(options: ConverseOptions<TAgentName>): Promise<ActorFlowVerdict>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Display/config metadata for an actor (from pikku.config.json). The email
|
|
29
|
+
* identifies the actor's user row; personality/jobTitle exist for the console
|
|
30
|
+
* screen and for agent-driven flows (the agent plays the persona).
|
|
31
|
+
*/
|
|
32
|
+
export interface ScenarioActorConfig {
|
|
33
|
+
email: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
jobTitle?: string;
|
|
36
|
+
personality?: string;
|
|
37
|
+
}
|
|
38
|
+
/** The injected `actors` service: actor name → actor. */
|
|
39
|
+
export type ScenarioActors = Record<string, ScenarioActor>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|