@pikku/core 0.12.139 → 0.12.142
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 +47 -0
- package/dist/errors/errors.d.ts +1 -1
- package/dist/errors/errors.js +1 -1
- package/dist/function/function-meta.types.d.ts +6 -6
- package/dist/function/functions.types.d.ts +2 -2
- package/dist/lifecycle.d.ts +6 -0
- package/dist/lifecycle.js +58 -0
- package/dist/services/audit-service.d.ts +4 -4
- package/dist/services/feature-flag-service.d.ts +3 -3
- package/dist/services/{http-personas.d.ts → http-virtual-users.d.ts} +25 -47
- package/dist/services/{http-personas.js → http-virtual-users.js} +29 -53
- package/dist/services/index.d.ts +2 -2
- package/dist/services/index.js +1 -1
- package/dist/services/meta-service.d.ts +6 -6
- package/dist/services/meta-service.js +8 -8
- package/dist/{wirings/workflow → services}/scenario-cookie-jar.d.ts +2 -0
- package/dist/{wirings/workflow → services}/scenario-cookie-jar.js +18 -14
- package/dist/services/scoped-secret-service.d.ts +1 -0
- package/dist/services/scoped-secret-service.js +3 -0
- package/dist/services/secret-service.d.ts +6 -0
- package/dist/services/system-role-guard.d.ts +1 -1
- package/dist/services/typed-secret-service.d.ts +1 -0
- package/dist/services/typed-secret-service.js +7 -0
- package/dist/services/virtual-user-sign-in.d.ts +52 -0
- package/dist/services/virtual-user-sign-in.js +80 -0
- package/dist/services/virtual-user-statement.d.ts +31 -0
- package/dist/services/virtual-user-statement.js +24 -0
- package/dist/services/{personas-service.d.ts → virtual-users-service.d.ts} +25 -23
- package/dist/services/{personas-service.js → virtual-users-service.js} +3 -3
- package/dist/testing/service-tests/secret-service-tests.js +9 -0
- package/dist/types/core.types.d.ts +11 -11
- package/dist/utils.d.ts +2 -5
- package/dist/utils.js +1 -38
- package/dist/wirings/cli/cli-runner.js +24 -9
- package/dist/wirings/http/http-runner.js +4 -0
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.js +12 -0
- package/dist/wirings/http/web-request.js +7 -0
- package/dist/wirings/role/define-system-role.d.ts +1 -1
- package/dist/wirings/role/define-system-role.js +1 -1
- package/dist/wirings/{actor-flow/actor-flow.types.d.ts → virtual-user/conversation/conversation.types.d.ts} +5 -5
- package/dist/wirings/virtual-user/conversation/index.d.ts +2 -0
- package/dist/wirings/virtual-user/conversation/run-conversation.d.ts +36 -0
- package/dist/wirings/{actor-flow → virtual-user/conversation}/run-conversation.js +30 -30
- package/dist/wirings/virtual-user/define-virtual-users.d.ts +32 -0
- package/dist/wirings/{persona/define-personas.js → virtual-user/define-virtual-users.js} +9 -9
- package/dist/wirings/virtual-user/index.d.ts +30 -7
- package/dist/wirings/virtual-user/index.js +23 -4
- package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +8 -8
- package/dist/wirings/virtual-user/prepare-virtual-user-run.js +9 -9
- package/dist/wirings/virtual-user/run-virtual-user.d.ts +6 -6
- package/dist/wirings/virtual-user/run-virtual-user.js +10 -10
- package/dist/wirings/{persona/validate-personas.d.ts → virtual-user/validate-virtual-users.d.ts} +11 -11
- package/dist/wirings/{persona/validate-personas.js → virtual-user/validate-virtual-users.js} +24 -24
- package/dist/wirings/virtual-user/virtual-user-agents.d.ts +7 -7
- package/dist/wirings/virtual-user/virtual-user-agents.js +2 -2
- package/dist/wirings/{persona/persona-app-scopes.d.ts → virtual-user/virtual-user-app-scopes.d.ts} +10 -10
- package/dist/wirings/{persona/persona-app-scopes.js → virtual-user/virtual-user-app-scopes.js} +12 -12
- package/dist/wirings/virtual-user/virtual-user-catalogue.d.ts +3 -3
- package/dist/wirings/virtual-user/virtual-user-catalogue.js +2 -2
- package/dist/wirings/{persona/persona.types.d.ts → virtual-user/virtual-user-declaration.types.d.ts} +24 -24
- package/dist/wirings/virtual-user/virtual-user-derive.js +13 -7
- package/dist/wirings/virtual-user/virtual-user-dispositions.d.ts +1 -1
- package/dist/wirings/{persona/persona-email.d.ts → virtual-user/virtual-user-email.d.ts} +11 -11
- package/dist/wirings/{persona/persona-email.js → virtual-user/virtual-user-email.js} +19 -19
- package/dist/wirings/{persona/persona-environments.d.ts → virtual-user/virtual-user-environments.d.ts} +10 -10
- package/dist/wirings/{persona/persona-environments.js → virtual-user/virtual-user-environments.js} +21 -21
- package/dist/wirings/virtual-user/virtual-user-intents.d.ts +3 -3
- package/dist/wirings/virtual-user/virtual-user-intents.js +3 -3
- package/dist/wirings/{persona/persona-mailbox.d.ts → virtual-user/virtual-user-mailbox.d.ts} +4 -4
- package/dist/wirings/virtual-user/virtual-user-run-store.d.ts +4 -4
- package/dist/wirings/virtual-user/virtual-user-scaffold.d.ts +42 -61
- package/dist/wirings/virtual-user/virtual-user-scaffold.js +69 -117
- package/dist/wirings/virtual-user/virtual-user-schedule-store.d.ts +15 -15
- package/dist/wirings/virtual-user/virtual-user-schedule.d.ts +6 -6
- package/dist/wirings/virtual-user/virtual-user-schedule.js +11 -11
- package/dist/wirings/virtual-user/virtual-user-target.d.ts +6 -6
- package/dist/wirings/virtual-user/virtual-user-target.js +5 -5
- package/dist/wirings/virtual-user/virtual-user.types.d.ts +11 -11
- package/dist/wirings/virtual-user/virtual-user.types.js +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +9 -9
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +28 -28
- package/dist/wirings/workflow/pikku-scenario-service.js +77 -77
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +4 -4
- package/dist/wirings/workflow/scenario-prose.d.ts +8 -8
- package/dist/wirings/workflow/scenario-prose.js +12 -8
- package/dist/wirings/workflow/scenario-run.types.d.ts +13 -13
- package/dist/wirings/workflow/scenario-step.types.d.ts +23 -23
- package/dist/wirings/workflow/workflow.types.d.ts +2 -2
- package/knowledge/decisions/index.md +0 -2
- package/knowledge/decisions/internals/a-credential-name-declared-twice-must-agree-on-schema-and-type.md +29 -0
- package/knowledge/decisions/internals/a-grant-can-name-a-scope-declared-without-its-addon.md +29 -0
- package/knowledge/decisions/internals/a-scheduled-task-writes-its-caller-session-before-it-runs.md +22 -0
- package/knowledge/decisions/internals/a-secret-set-in-memory-shadows-the-variable-of-the-same-name.md +28 -0
- package/knowledge/decisions/internals/a-virtual-user-acts-through-invokeraw-so-a-refusal-is-data.md +32 -0
- package/knowledge/decisions/internals/a-virtual-user-cadence-is-a-row-not-a-timer.md +12 -12
- package/knowledge/decisions/internals/a-virtual-user-converse-signs-in-before-its-first-turn.md +24 -0
- package/knowledge/decisions/internals/a-virtual-user-email-is-computed-with-the-run-id-as-a-sub-address.md +34 -0
- package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-but-it-needs-a-trigger.md +2 -2
- package/knowledge/decisions/internals/an-agent-is-offered-under-its-registry-key-and-only-within-the-virtual-users-scopes.md +27 -0
- package/knowledge/decisions/internals/an-oauth2-credential-implies-an-optional-app-secret.md +26 -0
- package/knowledge/decisions/internals/analytics-identity-resolvers-run-in-declared-order.md +22 -0
- package/knowledge/decisions/internals/feature-flag-declarations-refuse-an-empty-anyof-and-conflicting-scopes.md +22 -0
- package/knowledge/decisions/internals/feature-flags-fail-open-and-never-enforce-access.md +28 -0
- package/knowledge/decisions/internals/flag-sync-rewrites-only-the-declared-text.md +24 -0
- package/knowledge/decisions/internals/in-a-scenario-a-4xx-is-data-not-an-exception.md +3 -3
- package/knowledge/decisions/internals/index.md +46 -31
- package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +10 -10
- package/knowledge/decisions/internals/role-names-cannot-contain-the-scope-separator.md +30 -0
- package/knowledge/decisions/internals/scenario-agent-calls-sign-in-on-401-only.md +23 -19
- package/knowledge/decisions/internals/scenario-steps-return-drained-response-records.md +2 -2
- package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +12 -12
- package/knowledge/decisions/internals/the-core-scope-validator-refuses-separator-wildcard-and-empty-segments.md +22 -0
- package/knowledge/decisions/internals/the-pikku-user-id-resolves-in-a-fixed-order.md +24 -0
- package/knowledge/decisions/internals/{the-actor-prompt-says-json-because-of-json-object-mode.md → the-virtual-user-prompt-says-json-because-of-json-object-mode.md} +4 -4
- package/knowledge/decisions/internals/{the-persona-runtime-is-exported-from-the-persona-entry-point.md → the-virtual-user-runtime-is-exported-from-the-virtual-user-entry-point.md} +8 -8
- package/knowledge/decisions/internals/validate-checks-virtual-users-through-a-shared-module.md +43 -0
- package/knowledge/decisions/internals/{actor-flow-drives-the-target-through-a-transport-seam.md → virtual-user-conversation-drives-the-target-through-a-transport-seam.md} +6 -6
- package/knowledge/decisions/internals/virtual-user-conversation-verdicts-are-llm-self-evaluations.md +25 -0
- package/knowledge/decisions/internals/virtual-user-conversations-seed-a-hidden-kickoff-message.md +23 -0
- package/knowledge/decisions/internals/virtual-users-are-declared-in-one-definevirtualusers-call-per-codebase.md +30 -0
- package/knowledge/decisions/internals/workflow-core-never-imports-a-browser-driver.md +5 -5
- package/knowledge/decisions/internals/workflow-run-capabilities-are-extensions-not-subclasses.md +2 -2
- package/knowledge/decisions/internals/workflow-scenario-assertions-never-retry-and-record-one-step.md +4 -4
- package/knowledge/decisions/internals/workflow-scenario-hooks-are-a-scenario-only-affordance.md +1 -1
- package/knowledge/decisions/internals/workflow-scenario-prose-is-rendered-from-typed-calls-not-parsed-from-english.md +2 -2
- package/knowledge/decisions/internals/workflow-scenario-step-targets-are-string-literals-for-the-inspector.md +1 -1
- package/knowledge/decisions/internals/workflow-suspended-runs-keep-their-in-process-context.md +1 -1
- package/knowledge/decisions/security/a-bearer-token-checked-against-a-secret-reads-it-per-request.md +26 -0
- package/knowledge/decisions/security/a-call-without-a-session-raises-missing-session-not-forbidden.md +20 -0
- package/knowledge/decisions/security/a-caller-cookie-wins-over-the-jar-for-the-same-name.md +23 -0
- package/knowledge/decisions/security/a-channel-lifecycle-acts-as-the-socket-upgrade-session.md +16 -0
- package/knowledge/decisions/security/a-channel-requires-a-session-unless-it-says-auth-false.md +26 -0
- package/knowledge/decisions/security/a-credential-read-path-follows-its-declared-type-not-absence.md +32 -0
- package/knowledge/decisions/security/a-credential-type-is-a-literal-the-inspector-checks-at-declaration.md +27 -0
- package/knowledge/decisions/security/a-first-party-cookie-is-written-only-after-its-purposes-consent.md +33 -0
- package/knowledge/decisions/security/a-readonly-session-reaches-only-readonly-functions.md +25 -0
- package/knowledge/decisions/security/a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md +10 -5
- package/knowledge/decisions/security/a-scheduled-virtual-user-run-needs-no-admin-at-run-time.md +27 -0
- package/knowledge/decisions/security/a-scoped-credential-view-is-limited-to-declared-names.md +26 -0
- package/knowledge/decisions/security/a-scoped-secret-view-reads-only-declared-keys-and-never-writes.md +25 -0
- package/knowledge/decisions/security/a-secret-is-sent-only-to-the-hosts-it-declares.md +29 -0
- package/knowledge/decisions/security/a-step-can-name-an-identity-no-virtual-user-holds-through-per-call-headers.md +25 -0
- package/knowledge/decisions/security/a-stopped-run-by-id-skips-the-ownership-check.md +24 -0
- package/knowledge/decisions/security/a-vault-secret-redacts-on-serialization-and-throws-on-coercion.md +29 -0
- package/knowledge/decisions/security/a-virtual-user-catalogue-entry-is-filtered-by-scope-and-approval-is-denied-by-default.md +30 -0
- package/knowledge/decisions/security/a-virtual-user-environment-fails-closed.md +38 -0
- package/knowledge/decisions/security/a-virtual-user-never-runs-against-production.md +23 -0
- package/knowledge/decisions/security/a-virtual-user-reads-its-roles-from-the-apps-own-rpc-before-better-auth.md +27 -0
- package/knowledge/decisions/security/a-virtual-user-run-treats-an-unresolved-environment-as-production.md +41 -0
- package/knowledge/decisions/security/a-virtual-user-scopes-are-expanded-from-its-roles-once-at-run-preparation.md +25 -0
- package/knowledge/decisions/security/a-virtual-user-whose-roles-drift-from-the-declaration-refuses-to-run.md +32 -0
- package/knowledge/decisions/security/addon-screen-scopes-hide-and-do-not-enforce.md +20 -0
- package/knowledge/decisions/security/an-addon-header-can-only-narrow-the-session.md +26 -0
- package/knowledge/decisions/security/an-addon-instance-reads-only-the-secrets-and-credentials-it-is-named.md +33 -0
- package/knowledge/decisions/security/an-addon-may-reach-only-itself-and-the-addons-it-uses.md +16 -0
- package/knowledge/decisions/security/an-api-key-is-jwt-decoded-into-the-session.md +26 -0
- package/knowledge/decisions/security/an-external-virtual-user-caller-presents-one-signed-statement.md +38 -0
- package/knowledge/decisions/security/an-inbound-webhook-source-dispatches-only-verified-requests.md +31 -0
- package/knowledge/decisions/security/an-mcp-call-with-no-http-request-stays-anonymous.md +18 -0
- package/knowledge/decisions/security/an-omitted-user-id-addresses-the-platform-credential.md +32 -0
- package/knowledge/decisions/security/analytics-identity-is-stamped-from-the-session-not-the-event.md +24 -0
- package/knowledge/decisions/security/app-access-is-a-scope-tree-synthesised-from-virtual-users.md +26 -0
- package/knowledge/decisions/security/approval-policy-decides-who-answers-a-tool-call.md +27 -0
- package/knowledge/decisions/security/auth-false-on-a-session-function-warns-and-still-requires-one.md +25 -0
- package/knowledge/decisions/security/bearer-token-hashes-are-unpeppered-sha256.md +18 -0
- package/knowledge/decisions/security/consent-is-read-from-a-cookie-and-a-recognised-denial-counts-as-refusal.md +29 -0
- package/knowledge/decisions/security/cors-defaults-to-any-origin-and-refuses-the-wildcard-with-credentials.md +22 -0
- package/knowledge/decisions/security/credential-backends-share-one-conformance-suite-for-per-user-isolation.md +29 -0
- package/knowledge/decisions/security/each-stage-has-one-virtual-user-key-pair.md +27 -0
- package/knowledge/decisions/security/hmac-verification-fails-closed-on-empty-inputs.md +25 -0
- package/knowledge/decisions/security/index.md +88 -24
- package/knowledge/decisions/security/local-content-refuses-private-buckets-under-the-public-segment.md +21 -0
- package/knowledge/decisions/security/mail-is-allowlisted-before-a-virtual-user-run-sees-it.md +38 -0
- package/knowledge/decisions/security/mcp-resources-tools-and-prompts-take-sessionless-functions.md +18 -0
- package/knowledge/decisions/security/origin-locked-routes-match-the-origin-exactly.md +22 -0
- package/knowledge/decisions/security/oss-auth-works-without-pikku-studio-cloud.md +26 -0
- package/knowledge/decisions/security/outbound-webhooks-use-an-allowlist-when-configured.md +24 -0
- package/knowledge/decisions/security/pikku-carries-virtual-user-scopes-as-data-and-the-app-grants-them.md +35 -0
- package/knowledge/decisions/security/scoped-content-access-is-a-root-folder-with-longest-prefix-grants.md +31 -0
- package/knowledge/decisions/security/services-without-secrets-get-a-throwing-secrets-accessor.md +22 -0
- package/knowledge/decisions/security/shared-guards-refuse-to-mutate-or-shadow-a-system-role.md +34 -0
- package/knowledge/decisions/security/signed-uploads-are-verified-by-the-same-check-as-signed-reads.md +26 -0
- package/knowledge/decisions/security/system-role-scopes-must-be-declared-with-define-scope.md +24 -0
- package/knowledge/decisions/security/system-roles-are-additive-and-immutable.md +16 -0
- package/knowledge/decisions/security/the-channel-rpc-does-not-send-the-callers-session.md +21 -0
- package/knowledge/decisions/security/the-remote-jobs-inbox-fails-closed-without-a-secret.md +24 -0
- package/knowledge/decisions/security/the-remote-rpc-gate-fails-closed-and-binds-the-token-to-its-function.md +34 -0
- package/knowledge/decisions/security/the-workflow-status-stream-rechecks-ownership-on-every-poll.md +16 -0
- package/knowledge/decisions/security/two-credentials-on-one-key-must-share-a-read-mode.md +29 -0
- package/knowledge/decisions/security/verified-bearer-claims-are-pass-through-data-that-core-never-fills.md +23 -0
- package/knowledge/decisions/security/virtual-user-catalogue-excludes-the-machinery-that-runs-virtual-users.md +25 -0
- package/knowledge/decisions/security/{actor-flow-missing-approval-decisions-default-to-denied.md → virtual-user-conversation-missing-approval-decisions-default-to-denied.md} +7 -7
- package/knowledge/decisions/security/virtual-user-run-records-are-gated-behind-a-read-scope.md +18 -0
- package/knowledge/decisions/security/virtual-user-sign-in-does-not-exist-in-production.md +25 -0
- package/knowledge/decisions/security/virtual-user-sign-in-has-one-door.md +42 -0
- package/knowledge/decisions/security/{actor-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md → virtual-user-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md} +10 -8
- package/knowledge/decisions/security/virtual-user-sign-in-only-works-for-virtual-user-flagged-users.md +29 -0
- package/knowledge/decisions/security/workflow-helper-functions-are-sessionless-by-declaration.md +18 -0
- package/knowledge/decisions/security/{workflow-scenario-sessions-are-isolated-per-actor-and-per-scenario.md → workflow-scenario-sessions-are-isolated-per-virtual-user-and-per-scenario.md} +6 -6
- package/knowledge/decisions/security/workflow-scenario-steps-are-never-network-invocable.md +5 -5
- package/knowledge/decisions/security/{workflow-actor-steps-always-use-the-real-transport.md → workflow-virtual-user-steps-always-use-the-real-transport.md} +11 -11
- package/knowledge/decisions/security/writing-a-virtual-user-schedule-is-gated-by-its-own-scope.md +18 -0
- package/knowledge/index.md +0 -2
- package/knowledge/questions/a-bearer-jwt-with-no-identity-becomes-a-session.md +24 -0
- package/knowledge/questions/dev-quick-login-as-impersonation.md +25 -0
- package/knowledge/questions/does-a-delayed-rpc-run-as-the-session-it-was-scheduled-with.md +26 -0
- package/knowledge/questions/does-a-user-id-with-a-colon-read-another-users-credentials.md +27 -0
- package/knowledge/questions/does-a-wirecli-command-need-a-session-by-default.md +26 -0
- package/knowledge/questions/does-an-adversarial-in-scope-success-count-as-a-finding.md +26 -0
- package/knowledge/questions/does-approve-step-need-the-run-owners-session-or-the-gates-approvers.md +34 -0
- package/knowledge/questions/does-cors-origin-true-reflect-any-origin-with-credentials.md +20 -0
- package/knowledge/questions/does-skip-user-session-do-anything-on-a-channel.md +24 -0
- package/knowledge/questions/does-the-store-session-match-the-other-two.md +19 -0
- package/knowledge/questions/index.md +25 -2
- package/knowledge/questions/is-the-agent-score-worker-reachable-without-a-session.md +21 -0
- package/knowledge/questions/live-scheduled-tests-may-write.md +19 -0
- package/knowledge/questions/provision-virtual-users-has-no-caller-in-this-repo.md +23 -0
- package/knowledge/questions/remote-secret-refuses-requests-without-a-remote-token-on-every-path.md +32 -0
- package/knowledge/questions/should-a-middleware-set-session-persist-to-the-store.md +25 -0
- package/knowledge/questions/should-a-scoped-secret-view-throw-from-has-secret-for-a-disallowed-key.md +23 -0
- package/knowledge/questions/should-a-wrong-bearer-token-be-anonymous-or-refused.md +35 -0
- package/knowledge/questions/should-an-empty-webhook-signing-secret-send-unsigned.md +23 -0
- package/knowledge/questions/should-http-virtual-user-sessions-reset-between-scenarios-in-one-run.md +27 -0
- package/knowledge/questions/should-the-cookie-jar-scope-cookies-to-the-api-origin.md +26 -0
- package/knowledge/questions/should-voice-input-fetch-audio-urls-without-a-host-allowlist.md +20 -0
- package/knowledge/questions/staging-impersonation-rules.md +27 -0
- package/knowledge/questions/virtual-user-keys-on-cloud-stages-are-pikku-studios-to-build.md +34 -0
- package/knowledge/questions/virtual-user-statement-replay-is-per-process.md +20 -0
- package/knowledge/questions/what-an-impersonated-session-carries.md +22 -0
- package/package.json +3 -3
- package/src/public-surface.json +43 -47
- package/dist/services/persona-actor-secret.d.ts +0 -38
- package/dist/services/persona-actor-secret.js +0 -39
- package/dist/services/persona-sign-in.d.ts +0 -117
- package/dist/services/persona-sign-in.js +0 -154
- package/dist/wirings/actor-flow/index.d.ts +0 -2
- package/dist/wirings/actor-flow/run-conversation.d.ts +0 -36
- package/dist/wirings/persona/define-personas.d.ts +0 -32
- package/dist/wirings/persona/index.d.ts +0 -23
- package/dist/wirings/persona/index.js +0 -19
- package/knowledge/decisions/internals/actor-flow-conversations-seed-a-hidden-kickoff-message.md +0 -23
- package/knowledge/decisions/internals/actor-flow-verdicts-are-llm-self-evaluations.md +0 -25
- package/knowledge/decisions/internals/validate-checks-personas-through-a-shared-module.md +0 -43
- package/knowledge/decisions/security/actor-sign-in-only-works-for-actor-flagged-users.md +0 -31
- package/knowledge/decisions/security/an-actor-credential-is-derived-per-persona.md +0 -42
- package/knowledge/decisions/security/pikku-carries-actor-scopes-as-data-and-the-app-grants-them.md +0 -26
- /package/dist/wirings/{actor-flow/actor-flow.types.js → virtual-user/conversation/conversation.types.js} +0 -0
- /package/dist/wirings/{actor-flow → virtual-user/conversation}/index.js +0 -0
- /package/dist/wirings/{persona/persona.types.js → virtual-user/virtual-user-declaration.types.js} +0 -0
- /package/dist/wirings/{persona/persona-mailbox.js → virtual-user/virtual-user-mailbox.js} +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { inProcessSignIn, } from '../../services/virtual-user-sign-in.js';
|
|
1
2
|
import { readEnvVariable } from '../../env.js';
|
|
2
3
|
import { BadRequestError, ForbiddenError } from '../../errors/errors.js';
|
|
3
4
|
import { prepareVirtualUserRun } from './prepare-virtual-user-run.js';
|
|
4
5
|
import { runVirtualUser as runVirtualUserEngine } from './run-virtual-user.js';
|
|
5
|
-
import {
|
|
6
|
-
import { PRODUCTION_DISPOSITION } from './virtual-user.types.js';
|
|
6
|
+
import { virtualUserTarget } from './virtual-user-target.js';
|
|
7
7
|
/**
|
|
8
8
|
* The variables a scaffolded run reads.
|
|
9
9
|
*
|
|
@@ -18,55 +18,28 @@ export const VIRTUAL_USER_VARIABLES = {
|
|
|
18
18
|
* somewhere it only assumed was itself.
|
|
19
19
|
*/
|
|
20
20
|
apiUrl: 'VIRTUAL_USER_API_URL',
|
|
21
|
-
secret: 'SCENARIO_ACTOR_SECRET',
|
|
22
21
|
model: 'VIRTUAL_USER_MODEL',
|
|
23
22
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* auth somewhere other than the root — `/api/auth` is the common one — has no
|
|
27
|
-
* other way to say so, and without them the run signs in against a 404 and
|
|
28
|
-
* spends its whole budget thinking about why nothing works.
|
|
23
|
+
* Where the app serves its exposed RPCs, as a scenario run reads it. An app
|
|
24
|
+
* that mounts them somewhere other than `/rpc` has no other way to say so.
|
|
29
25
|
*/
|
|
30
|
-
signInPath: 'SCENARIO_SIGN_IN_PATH',
|
|
31
26
|
rpcPath: 'SCENARIO_RPC_PATH',
|
|
32
|
-
/**
|
|
33
|
-
* The deployed way in. A Fabric operator token is asymmetric — a stage can
|
|
34
|
-
* verify one and can never mint one — so unlike the actor secret it is safe
|
|
35
|
-
* for a run against a real environment. Read from the environment only as the
|
|
36
|
-
* fallback for a run nobody handed a token to, which is what a schedule is.
|
|
37
|
-
*/
|
|
38
|
-
operatorToken: 'FABRIC_OPERATOR_TOKEN',
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Which door under the auth mount, given the credential in hand.
|
|
42
|
-
*
|
|
43
|
-
* Both are better-auth plugins mounted side by side, so `SCENARIO_SIGN_IN_PATH`
|
|
44
|
-
* names the mount and the last segment is ours to pick — an app that moved auth
|
|
45
|
-
* to `/api/auth` says so once and both paths follow. A path that names neither
|
|
46
|
-
* plugin is left alone, since it was configured deliberately.
|
|
47
|
-
*/
|
|
48
|
-
export const signInPathFor = (configured, plugin) => {
|
|
49
|
-
if (!configured) {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
const mount = configured.replace(/\/sign-in\/(actor|fabric)$/, '');
|
|
53
|
-
return mount === configured ? configured : `${mount}/sign-in/${plugin}`;
|
|
54
27
|
};
|
|
55
28
|
/**
|
|
56
|
-
* The declared
|
|
29
|
+
* The declared virtual user behind an id, refused unless it is one a run may be.
|
|
57
30
|
*
|
|
58
|
-
* An acted-upon
|
|
31
|
+
* An acted-upon virtual user has no session of its own, and running one would race
|
|
59
32
|
* whatever scenario acts on it.
|
|
60
33
|
*/
|
|
61
|
-
export const
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
64
|
-
throw new BadRequestError(`Unknown
|
|
34
|
+
export const runnableVirtualUser = (virtualUsers, virtualUserId) => {
|
|
35
|
+
const virtualUser = virtualUsers[virtualUserId];
|
|
36
|
+
if (!virtualUser) {
|
|
37
|
+
throw new BadRequestError(`Unknown virtual user "${virtualUserId}" — declare it with defineVirtualUsers()`);
|
|
65
38
|
}
|
|
66
|
-
if (!
|
|
67
|
-
throw new BadRequestError(`
|
|
39
|
+
if (!virtualUser.runnable) {
|
|
40
|
+
throw new BadRequestError(`VirtualUser "${virtualUserId}" is declared as acted upon, never run`);
|
|
68
41
|
}
|
|
69
|
-
return
|
|
42
|
+
return virtualUser;
|
|
70
43
|
};
|
|
71
44
|
const MISSING_RUN_STORE = 'No virtualUserRunStore is wired — a run has nowhere to be recorded. ' +
|
|
72
45
|
'Wire KyselyVirtualUserRunStore from @pikku/kysely, or your own implementation of VirtualUserRunStore.';
|
|
@@ -94,7 +67,7 @@ export const requireVirtualUserScheduleStore = (store) => {
|
|
|
94
67
|
* `NODE_ENV=production` there too — keying on it refuses every disposition on
|
|
95
68
|
* the one environment they exist to be used on. `PIKKU_ENV` names which of the
|
|
96
69
|
* configured environments this is, which is the question actually being asked,
|
|
97
|
-
* and it is the same signal `
|
|
70
|
+
* and it is the same signal `virtualUserEnvironmentRefusal` already checks at
|
|
98
71
|
* sign-in.
|
|
99
72
|
*
|
|
100
73
|
* Unresolved is treated as production: an environment nobody can name is one
|
|
@@ -108,12 +81,11 @@ const isProductionRun = (config, environments, environment) => {
|
|
|
108
81
|
}
|
|
109
82
|
return environment ? Boolean(environments[environment]?.production) : true;
|
|
110
83
|
};
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
const refuseInProduction = (
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
throw new ForbiddenError(`Only the '${PRODUCTION_DISPOSITION}' disposition may run against production; "${personaId}" is ${disposition}`);
|
|
84
|
+
// A virtual user is a probe, and probing is not a thing to do to real
|
|
85
|
+
// customers' data, whatever the disposition.
|
|
86
|
+
const refuseInProduction = (virtualUserId, config, environments, environment) => {
|
|
87
|
+
if (isProductionRun(config, environments, environment)) {
|
|
88
|
+
throw new ForbiddenError(`Virtual users do not run against production; "${virtualUserId}" cannot run here`);
|
|
117
89
|
}
|
|
118
90
|
};
|
|
119
91
|
/**
|
|
@@ -123,15 +95,13 @@ const refuseInProduction = (personaId, disposition, config, environments, enviro
|
|
|
123
95
|
* scheduled tick have in common. The dispatch that follows is typed off the
|
|
124
96
|
* app's RPC map, so it stays in the generated wiring.
|
|
125
97
|
*/
|
|
126
|
-
export const startVirtualUserRun = async ({ store,
|
|
98
|
+
export const startVirtualUserRun = async ({ store, virtualUsers, config, environments, environment = readEnvVariable('PIKKU_ENV'), virtualUserId, disposition: requested, seed: requestedSeed, goals, memory, startedBy, }) => {
|
|
127
99
|
const runStore = requireVirtualUserRunStore(store);
|
|
128
|
-
const
|
|
100
|
+
const virtualUser = runnableVirtualUser(virtualUsers, virtualUserId);
|
|
129
101
|
const disposition = (requested ??
|
|
130
|
-
|
|
102
|
+
virtualUser.disposition ??
|
|
131
103
|
'realistic');
|
|
132
|
-
|
|
133
|
-
// one in.
|
|
134
|
-
refuseInProduction(personaId, disposition, config, environments, environment);
|
|
104
|
+
refuseInProduction(virtualUserId, config, environments, environment);
|
|
135
105
|
// Seeded here rather than inside the engine so the record carries the seed
|
|
136
106
|
// even if the run dies before returning — an unreproducible crash costs the
|
|
137
107
|
// most.
|
|
@@ -139,7 +109,7 @@ export const startVirtualUserRun = async ({ store, personas, config, environment
|
|
|
139
109
|
const resolvedGoals = goals ?? [];
|
|
140
110
|
const resolvedMemory = memory ?? {};
|
|
141
111
|
const runId = await runStore.start({
|
|
142
|
-
|
|
112
|
+
virtualUserId: virtualUser.id,
|
|
143
113
|
disposition,
|
|
144
114
|
seed,
|
|
145
115
|
goals: resolvedGoals,
|
|
@@ -148,7 +118,7 @@ export const startVirtualUserRun = async ({ store, personas, config, environment
|
|
|
148
118
|
});
|
|
149
119
|
return {
|
|
150
120
|
runId,
|
|
151
|
-
|
|
121
|
+
virtualUserId: virtualUser.id,
|
|
152
122
|
disposition,
|
|
153
123
|
seed,
|
|
154
124
|
goals: resolvedGoals,
|
|
@@ -164,7 +134,7 @@ export const startVirtualUserRun = async ({ store, personas, config, environment
|
|
|
164
134
|
*/
|
|
165
135
|
export const serializeVirtualUserRun = (run) => ({
|
|
166
136
|
runId: run.runId,
|
|
167
|
-
|
|
137
|
+
virtualUserId: run.virtualUserId,
|
|
168
138
|
disposition: run.disposition,
|
|
169
139
|
seed: run.seed,
|
|
170
140
|
status: run.status,
|
|
@@ -212,10 +182,10 @@ export const serializeVirtualUserSteps = (steps) => steps.map((step) => ({
|
|
|
212
182
|
* takes; the engine's own duration also accepts `'30m'`, which nothing on this
|
|
213
183
|
* side ever writes.
|
|
214
184
|
*/
|
|
215
|
-
export const serializeVirtualUserSchedule = (schedule,
|
|
216
|
-
const
|
|
185
|
+
export const serializeVirtualUserSchedule = (schedule, virtualUsers) => {
|
|
186
|
+
const virtualUser = virtualUsers[schedule.virtualUserId];
|
|
217
187
|
return {
|
|
218
|
-
|
|
188
|
+
virtualUserId: schedule.virtualUserId,
|
|
219
189
|
enabled: schedule.enabled,
|
|
220
190
|
disposition: schedule.disposition,
|
|
221
191
|
goals: schedule.goals,
|
|
@@ -234,36 +204,36 @@ export const serializeVirtualUserSchedule = (schedule, personas) => {
|
|
|
234
204
|
lastRunId: schedule.lastRunId,
|
|
235
205
|
lastRunAt: schedule.lastRunAt ? schedule.lastRunAt.toISOString() : null,
|
|
236
206
|
declared: {
|
|
237
|
-
disposition: (
|
|
207
|
+
disposition: (virtualUser?.disposition ??
|
|
238
208
|
'realistic'),
|
|
239
|
-
goals:
|
|
209
|
+
goals: virtualUser?.goals ?? [],
|
|
240
210
|
},
|
|
241
211
|
};
|
|
242
212
|
};
|
|
243
213
|
/**
|
|
244
|
-
* Writes a
|
|
214
|
+
* Writes a virtual user's cadence.
|
|
245
215
|
*
|
|
246
216
|
* Applies the same rules `startVirtualUserRun` enforces, at the point the row is
|
|
247
|
-
* written rather than every hour afterwards: an acted-upon
|
|
248
|
-
* session, and
|
|
249
|
-
*
|
|
217
|
+
* written rather than every hour afterwards: an acted-upon virtual user has no
|
|
218
|
+
* session, and production refuses every run, so either is a tick that can only
|
|
219
|
+
* ever fail to start, with nobody watching.
|
|
250
220
|
*
|
|
251
|
-
*
|
|
252
|
-
* keeps what it had: enabling a row saved earlier as
|
|
253
|
-
*
|
|
221
|
+
* Whether the row is enabled is the one thing checked, since a field left out
|
|
222
|
+
* keeps what it had: enabling a row saved earlier is refused just as saving a
|
|
223
|
+
* new one would be.
|
|
254
224
|
*/
|
|
255
|
-
export const writeVirtualUserSchedule = async ({ store,
|
|
225
|
+
export const writeVirtualUserSchedule = async ({ store, virtualUsers, virtualUserId, enabled, disposition, goals, budget, minIntervalMs, maxIntervalMs, nextRunAt, config, environments, environment = readEnvVariable('PIKKU_ENV'), }) => {
|
|
256
226
|
const scheduleStore = requireVirtualUserScheduleStore(store);
|
|
257
|
-
const declared =
|
|
227
|
+
const declared = runnableVirtualUser(virtualUsers, virtualUserId);
|
|
258
228
|
const current = enabled === undefined || disposition === undefined
|
|
259
|
-
? await scheduleStore.get(
|
|
229
|
+
? await scheduleStore.get(virtualUserId)
|
|
260
230
|
: null;
|
|
261
231
|
const created = disposition === undefined && !current ? declared.disposition : undefined;
|
|
262
232
|
if (enabled ?? current?.enabled) {
|
|
263
|
-
refuseInProduction(
|
|
233
|
+
refuseInProduction(virtualUserId, config, environments, environment);
|
|
264
234
|
}
|
|
265
235
|
return scheduleStore.set({
|
|
266
|
-
|
|
236
|
+
virtualUserId,
|
|
267
237
|
enabled,
|
|
268
238
|
disposition: (disposition ?? created),
|
|
269
239
|
goals,
|
|
@@ -283,7 +253,7 @@ export const writeVirtualUserSchedule = async ({ store, personas, persona, enabl
|
|
|
283
253
|
};
|
|
284
254
|
/** What a due schedule asks `runVirtualUser` for. */
|
|
285
255
|
export const virtualUserScheduleRunInput = (schedule) => ({
|
|
286
|
-
|
|
256
|
+
virtualUserId: schedule.virtualUserId,
|
|
287
257
|
disposition: schedule.disposition,
|
|
288
258
|
goals: schedule.goals,
|
|
289
259
|
budget: schedule.budget
|
|
@@ -300,28 +270,28 @@ export const virtualUserScheduleRunInput = (schedule) => ({
|
|
|
300
270
|
* What a tick did.
|
|
301
271
|
*
|
|
302
272
|
* Logged rather than returned: the caller is a cron, and a run this started is
|
|
303
|
-
* otherwise the only trace that a
|
|
273
|
+
* otherwise the only trace that a virtual user is still out there working.
|
|
304
274
|
*/
|
|
305
275
|
export const logVirtualUserTick = (logger, result) => {
|
|
306
|
-
for (const {
|
|
307
|
-
logger.info(`Virtual user ${
|
|
276
|
+
for (const { virtualUserId, runId } of result.dispatched) {
|
|
277
|
+
logger.info(`Virtual user ${virtualUserId} started run ${runId} on schedule`);
|
|
308
278
|
}
|
|
309
279
|
for (const runId of result.reaped) {
|
|
310
|
-
logger.warn(`Virtual user run ${runId} was abandoned — marked failed so its
|
|
280
|
+
logger.warn(`Virtual user run ${runId} was abandoned — marked failed so its virtual user can run again`);
|
|
311
281
|
}
|
|
312
|
-
for (const {
|
|
313
|
-
logger.info(`Virtual user ${
|
|
282
|
+
for (const { virtualUserId, reason } of result.skipped) {
|
|
283
|
+
logger.info(`Virtual user ${virtualUserId} skipped this tick: ${reason}`);
|
|
314
284
|
}
|
|
315
285
|
};
|
|
316
286
|
/**
|
|
317
287
|
* The run itself.
|
|
318
288
|
*
|
|
319
289
|
* Everything it needs is derived through `metaService` and the generated
|
|
320
|
-
*
|
|
290
|
+
* virtual users — the same public surface any consumer has. Nothing reaches into
|
|
321
291
|
* pikku's internals, because an app could not, and a feature built on what only
|
|
322
292
|
* the framework can see would not be this feature.
|
|
323
293
|
*/
|
|
324
|
-
export const executeVirtualUserRun = async ({ runStore, metaService, agentRunner, variables, logger,
|
|
294
|
+
export const executeVirtualUserRun = async ({ runStore, metaService, agentRunner, variables, logger, virtualUsers, createVirtualUsers, runId, virtualUserId, disposition, goals, memory, seed, budget, auth, }) => {
|
|
325
295
|
const store = requireVirtualUserRunStore(runStore);
|
|
326
296
|
try {
|
|
327
297
|
if (!metaService) {
|
|
@@ -332,20 +302,11 @@ export const executeVirtualUserRun = async ({ runStore, metaService, agentRunner
|
|
|
332
302
|
}
|
|
333
303
|
const apiUrl = await variables.get(VIRTUAL_USER_VARIABLES.apiUrl);
|
|
334
304
|
if (!apiUrl) {
|
|
335
|
-
throw new Error(`${VIRTUAL_USER_VARIABLES.apiUrl} is not set
|
|
305
|
+
throw new Error(`${VIRTUAL_USER_VARIABLES.apiUrl} is not set, so the virtual user has no address to drive. ` +
|
|
306
|
+
`Set it to the URL this app is reached at, including any HTTP prefix (for example https://my-app.example.com/api). \`pikku dev\` sets it for you.`);
|
|
336
307
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// secret is the local-only fallback, because only `pikku dev` serves the
|
|
340
|
-
// endpoint that accepts it.
|
|
341
|
-
const token = operatorToken ??
|
|
342
|
-
(await variables.get(VIRTUAL_USER_VARIABLES.operatorToken));
|
|
343
|
-
const secret = token
|
|
344
|
-
? undefined
|
|
345
|
-
: await variables.get(VIRTUAL_USER_VARIABLES.secret);
|
|
346
|
-
if (!token && !secret) {
|
|
347
|
-
throw new Error(`Neither an operator token nor ${VIRTUAL_USER_VARIABLES.secret} is available — there is nobody for the virtual user to be. ` +
|
|
348
|
-
`Hand a Fabric operator token in with the run against a deployed stage, or export ${VIRTUAL_USER_VARIABLES.secret} against a local \`pikku dev\` target.`);
|
|
308
|
+
if (!auth) {
|
|
309
|
+
throw new Error('The app has no auth service, so the virtual user has no way to sign in. Register better-auth (with the pikkuVirtualUser plugin) as the auth singleton service.');
|
|
349
310
|
}
|
|
350
311
|
const model = await variables.get(VIRTUAL_USER_VARIABLES.model);
|
|
351
312
|
if (!model) {
|
|
@@ -358,40 +319,31 @@ export const executeVirtualUserRun = async ({ runStore, metaService, agentRunner
|
|
|
358
319
|
...new Set(Object.values(functionsMeta).flatMap((meta) => [meta.inputSchemaName, meta.outputSchemaName].filter((name) => !!name))),
|
|
359
320
|
];
|
|
360
321
|
const schemas = (await metaService.getSchemas(schemaNames));
|
|
361
|
-
const
|
|
362
|
-
if (!
|
|
363
|
-
throw new Error(`
|
|
322
|
+
const virtualUser = virtualUsers[virtualUserId];
|
|
323
|
+
if (!virtualUser) {
|
|
324
|
+
throw new Error(`VirtualUser "${virtualUserId}" is no longer declared`);
|
|
364
325
|
}
|
|
365
326
|
const { catalogue, intents, agents } = prepareVirtualUserRun({
|
|
366
|
-
|
|
327
|
+
virtualUser,
|
|
367
328
|
functionsMeta,
|
|
368
329
|
schemas,
|
|
369
330
|
workflowsMeta: await metaService.getWorkflowMeta(),
|
|
370
331
|
systemRoles: await metaService.getSystemRolesMeta(),
|
|
371
332
|
agentsMeta: await metaService.getAgentsMeta(),
|
|
372
333
|
});
|
|
373
|
-
const
|
|
374
|
-
const signedIn = createPersonas({
|
|
334
|
+
const signedIn = createVirtualUsers({
|
|
375
335
|
apiUrl,
|
|
376
|
-
|
|
377
|
-
? {
|
|
378
|
-
operator: {
|
|
379
|
-
token,
|
|
380
|
-
signInPath: signInPathFor(configuredSignInPath, 'fabric'),
|
|
381
|
-
},
|
|
382
|
-
}
|
|
383
|
-
: { secret }),
|
|
336
|
+
signIn: inProcessSignIn(auth),
|
|
384
337
|
model,
|
|
385
|
-
signInPath: signInPathFor(configuredSignInPath, 'actor'),
|
|
386
338
|
rpcPath: (await variables.get(VIRTUAL_USER_VARIABLES.rpcPath)) ?? undefined,
|
|
387
339
|
});
|
|
388
|
-
const target = signedIn[
|
|
340
|
+
const target = signedIn[virtualUserId];
|
|
389
341
|
if (!target) {
|
|
390
|
-
throw new Error(`
|
|
342
|
+
throw new Error(`VirtualUser "${virtualUserId}" cannot sign in`);
|
|
391
343
|
}
|
|
392
344
|
const result = await runVirtualUserEngine({
|
|
393
|
-
|
|
394
|
-
|
|
345
|
+
virtualUser,
|
|
346
|
+
virtualUserId,
|
|
395
347
|
disposition: disposition,
|
|
396
348
|
catalogue,
|
|
397
349
|
intents,
|
|
@@ -399,11 +351,11 @@ export const executeVirtualUserRun = async ({ runStore, metaService, agentRunner
|
|
|
399
351
|
memory,
|
|
400
352
|
seed,
|
|
401
353
|
agents,
|
|
402
|
-
target:
|
|
354
|
+
target: virtualUserTarget(target, {
|
|
403
355
|
model,
|
|
404
356
|
agents: agents.map((agent) => agent.name),
|
|
405
357
|
}),
|
|
406
|
-
// `AgentRunnerService.run` IS the engine's `
|
|
358
|
+
// `AgentRunnerService.run` IS the engine's `VirtualUserLLM` — same params, same
|
|
407
359
|
// result — so a virtual user thinks through the same runner every agent in
|
|
408
360
|
// the app does, provider quirks and all.
|
|
409
361
|
llm: (params) => agentRunner.run(params),
|
|
@@ -429,7 +381,7 @@ export const executeVirtualUserRun = async ({ runStore, metaService, agentRunner
|
|
|
429
381
|
// record is the only place that distinction survives — leaving it at
|
|
430
382
|
// 'running' forever is what `fail` exists to prevent.
|
|
431
383
|
const message = error instanceof Error ? error.message : String(error);
|
|
432
|
-
logger.error(`Virtual user run ${runId} (${
|
|
384
|
+
logger.error(`Virtual user run ${runId} (${virtualUserId}) failed: ${message}`);
|
|
433
385
|
await store.fail(runId, message);
|
|
434
386
|
throw error;
|
|
435
387
|
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import type { VirtualUserBudget, VirtualUserDisposition } from './virtual-user.types.js';
|
|
2
2
|
/**
|
|
3
|
-
* One
|
|
3
|
+
* One virtual user's standing instruction to keep using the app.
|
|
4
4
|
*
|
|
5
5
|
* A virtual user that runs once tells you about one afternoon. What an app
|
|
6
|
-
* actually wants to know is what a
|
|
6
|
+
* actually wants to know is what a virtual user hits over a fortnight, and that is
|
|
7
7
|
* a cadence, not a longer run — a budget already caps how far a single run
|
|
8
8
|
* goes, and raising it only buys a more tired user.
|
|
9
9
|
*
|
|
10
10
|
* The row is the schedule. There is deliberately no timer, interval or
|
|
11
11
|
* in-memory loop anywhere near it: a process that holds the next run in its own
|
|
12
|
-
* heap forgets it on the next deploy, and a
|
|
12
|
+
* heap forgets it on the next deploy, and a virtual user silently stops. Something
|
|
13
13
|
* outside asks which rows are due; the answer survives restarts because it is
|
|
14
14
|
* written down.
|
|
15
15
|
*/
|
|
16
16
|
export interface VirtualUserScheduleRecord {
|
|
17
|
-
|
|
17
|
+
virtualUserId: string;
|
|
18
18
|
/**
|
|
19
19
|
* Off by default. A schedule that ran the moment it was written would start
|
|
20
20
|
* spending an app's model budget as a side effect of a migration.
|
|
@@ -24,20 +24,20 @@ export interface VirtualUserScheduleRecord {
|
|
|
24
24
|
goals: string[];
|
|
25
25
|
budget: VirtualUserBudget | null;
|
|
26
26
|
/**
|
|
27
|
-
* The gap to the next run is drawn between these, not fixed. A
|
|
27
|
+
* The gap to the next run is drawn between these, not fixed. A virtual user that
|
|
28
28
|
* appears at exactly 09:00 every day exercises one cache state and one cron
|
|
29
29
|
* neighbourhood; a real one does not keep an appointment.
|
|
30
30
|
*/
|
|
31
31
|
minIntervalMs: number;
|
|
32
32
|
maxIntervalMs: number;
|
|
33
|
-
/** When this
|
|
33
|
+
/** When this virtual user is next allowed to run. The whole schedule, in one field. */
|
|
34
34
|
nextRunAt: Date;
|
|
35
35
|
lastRunId: string | null;
|
|
36
36
|
lastRunAt: Date | null;
|
|
37
37
|
}
|
|
38
38
|
/** A partial write — anything left out keeps whatever the row already had. */
|
|
39
39
|
export interface VirtualUserScheduleInput {
|
|
40
|
-
|
|
40
|
+
virtualUserId: string;
|
|
41
41
|
enabled?: boolean;
|
|
42
42
|
disposition?: VirtualUserDisposition;
|
|
43
43
|
goals?: readonly string[];
|
|
@@ -57,33 +57,33 @@ export interface VirtualUserScheduleInput {
|
|
|
57
57
|
* smaller permission than deciding they should keep going.
|
|
58
58
|
*/
|
|
59
59
|
export interface VirtualUserScheduleStore {
|
|
60
|
-
/** Creates or updates one
|
|
60
|
+
/** Creates or updates one virtual user's cadence. Returns the row as it now stands. */
|
|
61
61
|
set(schedule: VirtualUserScheduleInput): Promise<VirtualUserScheduleRecord>;
|
|
62
|
-
get(
|
|
62
|
+
get(virtualUserId: string): Promise<VirtualUserScheduleRecord | null>;
|
|
63
63
|
list(): Promise<VirtualUserScheduleRecord[]>;
|
|
64
64
|
/** Enabled rows whose `nextRunAt` has passed. */
|
|
65
65
|
due(now: Date): Promise<VirtualUserScheduleRecord[]>;
|
|
66
66
|
/**
|
|
67
|
-
* Pushes the
|
|
67
|
+
* Pushes the virtual user's next run out, and records which run this was.
|
|
68
68
|
*
|
|
69
69
|
* Called *before* the run is dispatched, so a tick that dies halfway does not
|
|
70
70
|
* leave a row due and get re-dispatched by the next one. The cost is that a
|
|
71
71
|
* dispatch which throws waits a full interval rather than retrying, which is
|
|
72
|
-
* the right way round: a
|
|
72
|
+
* the right way round: a virtual user that is failing to start should not be
|
|
73
73
|
* retried every minute for a week.
|
|
74
74
|
*
|
|
75
75
|
* `from` is the `nextRunAt` the caller read, and the write must match it to
|
|
76
|
-
* land — the claim is how a tick wins the
|
|
76
|
+
* land — the claim is how a tick wins the virtual user, not just how it records
|
|
77
77
|
* winning. Two processes on the same cron read the same due row, and without
|
|
78
78
|
* the compare-and-set both would dispatch: the same user acting twice over,
|
|
79
79
|
* at twice the budget, producing findings neither run can reproduce. The
|
|
80
|
-
* loser is told `false` and leaves the
|
|
80
|
+
* loser is told `false` and leaves the virtual user to whoever got there first.
|
|
81
81
|
*/
|
|
82
|
-
claim(
|
|
82
|
+
claim(virtualUserId: string, claim: {
|
|
83
83
|
from: Date;
|
|
84
84
|
nextRunAt: Date;
|
|
85
85
|
runId: string | null;
|
|
86
86
|
at: Date;
|
|
87
87
|
}): Promise<boolean>;
|
|
88
|
-
remove(
|
|
88
|
+
remove(virtualUserId: string): Promise<void>;
|
|
89
89
|
}
|
|
@@ -6,7 +6,7 @@ import type { VirtualUserScheduleRecord, VirtualUserScheduleStore } from './virt
|
|
|
6
6
|
*
|
|
7
7
|
* A run holds no process across a restart — see {@link VirtualUserRunRecord} —
|
|
8
8
|
* so a deploy mid-run strands the record, and a stranded record would block its
|
|
9
|
-
*
|
|
9
|
+
* virtual user's schedule for good. Twice the longest duration budget anyone sets in
|
|
10
10
|
* practice, because the failure this guards against is cheap to recover from
|
|
11
11
|
* and expensive to trigger early: reaping a run that was still working loses
|
|
12
12
|
* its findings.
|
|
@@ -25,7 +25,7 @@ export declare const DEFAULT_MAX_INTERVAL_MS: number;
|
|
|
25
25
|
/** Whether a row is the tick's business, for stores that cannot ask in a query. */
|
|
26
26
|
export declare const isDue: (schedule: VirtualUserScheduleRecord, now: Date) => boolean;
|
|
27
27
|
/**
|
|
28
|
-
* When this
|
|
28
|
+
* When this virtual user should next appear, drawn from its own interval.
|
|
29
29
|
*
|
|
30
30
|
* Uniform between the two bounds. Reversed bounds are read as a range rather
|
|
31
31
|
* than rejected, because a schedule is configuration and a swapped pair is a
|
|
@@ -35,11 +35,11 @@ export declare const nextRunAt: (schedule: Pick<VirtualUserScheduleRecord, 'minI
|
|
|
35
35
|
export type VirtualUserSkipReason = 'in-flight' | 'dispatch-failed' | 'claimed-elsewhere';
|
|
36
36
|
export interface VirtualUserTickResult {
|
|
37
37
|
dispatched: {
|
|
38
|
-
|
|
38
|
+
virtualUserId: string;
|
|
39
39
|
runId: string;
|
|
40
40
|
}[];
|
|
41
41
|
skipped: {
|
|
42
|
-
|
|
42
|
+
virtualUserId: string;
|
|
43
43
|
reason: VirtualUserSkipReason;
|
|
44
44
|
}[];
|
|
45
45
|
/** Runs found stranded at `running` and marked failed. */
|
|
@@ -55,14 +55,14 @@ export interface VirtualUserTickParams {
|
|
|
55
55
|
staleAfterMs?: number;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Acts on whichever
|
|
58
|
+
* Acts on whichever virtual users are due, once.
|
|
59
59
|
*
|
|
60
60
|
* The whole cadence lives in this one call, so what schedules it is the host's
|
|
61
61
|
* choice — a cron wiring, a platform scheduler, or a person clicking a button.
|
|
62
62
|
* Pikku does not start a timer on an app's behalf; a scaffold that did would
|
|
63
63
|
* begin spending model budget the moment a project ran `pikku all`.
|
|
64
64
|
*
|
|
65
|
-
* A
|
|
65
|
+
* A virtual user is skipped, not queued, while its previous run is still going. Two
|
|
66
66
|
* copies of the same user acting at once is not a heavier test, it is a
|
|
67
67
|
* different one, and every finding it produces is unreproducible. Two ticks
|
|
68
68
|
* running at once are held to the same rule by the claim, which only lands for
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A run holds no process across a restart — see {@link VirtualUserRunRecord} —
|
|
6
6
|
* so a deploy mid-run strands the record, and a stranded record would block its
|
|
7
|
-
*
|
|
7
|
+
* virtual user's schedule for good. Twice the longest duration budget anyone sets in
|
|
8
8
|
* practice, because the failure this guards against is cheap to recover from
|
|
9
9
|
* and expensive to trigger early: reaping a run that was still working loses
|
|
10
10
|
* its findings.
|
|
@@ -23,7 +23,7 @@ export const DEFAULT_MAX_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
|
23
23
|
/** Whether a row is the tick's business, for stores that cannot ask in a query. */
|
|
24
24
|
export const isDue = (schedule, now) => schedule.enabled && schedule.nextRunAt.getTime() <= now.getTime();
|
|
25
25
|
/**
|
|
26
|
-
* When this
|
|
26
|
+
* When this virtual user should next appear, drawn from its own interval.
|
|
27
27
|
*
|
|
28
28
|
* Uniform between the two bounds. Reversed bounds are read as a range rather
|
|
29
29
|
* than rejected, because a schedule is configuration and a swapped pair is a
|
|
@@ -35,14 +35,14 @@ export const nextRunAt = (schedule, now, random) => {
|
|
|
35
35
|
return new Date(now.getTime() + low + random() * (high - low));
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
|
-
* Acts on whichever
|
|
38
|
+
* Acts on whichever virtual users are due, once.
|
|
39
39
|
*
|
|
40
40
|
* The whole cadence lives in this one call, so what schedules it is the host's
|
|
41
41
|
* choice — a cron wiring, a platform scheduler, or a person clicking a button.
|
|
42
42
|
* Pikku does not start a timer on an app's behalf; a scaffold that did would
|
|
43
43
|
* begin spending model budget the moment a project ran `pikku all`.
|
|
44
44
|
*
|
|
45
|
-
* A
|
|
45
|
+
* A virtual user is skipped, not queued, while its previous run is still going. Two
|
|
46
46
|
* copies of the same user acting at once is not a heavier test, it is a
|
|
47
47
|
* different one, and every finding it produces is unreproducible. Two ticks
|
|
48
48
|
* running at once are held to the same rule by the claim, which only lands for
|
|
@@ -55,17 +55,17 @@ export const tickVirtualUserSchedules = async ({ schedules, runs, dispatch, now
|
|
|
55
55
|
reaped: [],
|
|
56
56
|
};
|
|
57
57
|
for (const schedule of await schedules.due(now)) {
|
|
58
|
-
const [latest] = await runs.list({
|
|
58
|
+
const [latest] = await runs.list({ virtualUserId: schedule.virtualUserId, limit: 1 });
|
|
59
59
|
if (latest?.status === 'running') {
|
|
60
60
|
if (now.getTime() - latest.createdAt.getTime() < staleAfterMs) {
|
|
61
|
-
result.skipped.push({
|
|
61
|
+
result.skipped.push({ virtualUserId: schedule.virtualUserId, reason: 'in-flight' });
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
64
|
await runs.fail(latest.runId, `Abandoned: still running ${Math.round((now.getTime() - latest.createdAt.getTime()) / 60000)}m after it started, which is longer than any budget allows.`);
|
|
65
65
|
result.reaped.push(latest.runId);
|
|
66
66
|
}
|
|
67
67
|
const due = nextRunAt(schedule, now, random);
|
|
68
|
-
const acquired = await schedules.claim(schedule.
|
|
68
|
+
const acquired = await schedules.claim(schedule.virtualUserId, {
|
|
69
69
|
from: schedule.nextRunAt,
|
|
70
70
|
nextRunAt: due,
|
|
71
71
|
runId: null,
|
|
@@ -73,7 +73,7 @@ export const tickVirtualUserSchedules = async ({ schedules, runs, dispatch, now
|
|
|
73
73
|
});
|
|
74
74
|
if (!acquired) {
|
|
75
75
|
result.skipped.push({
|
|
76
|
-
|
|
76
|
+
virtualUserId: schedule.virtualUserId,
|
|
77
77
|
reason: 'claimed-elsewhere',
|
|
78
78
|
});
|
|
79
79
|
continue;
|
|
@@ -84,18 +84,18 @@ export const tickVirtualUserSchedules = async ({ schedules, runs, dispatch, now
|
|
|
84
84
|
}
|
|
85
85
|
catch {
|
|
86
86
|
result.skipped.push({
|
|
87
|
-
|
|
87
|
+
virtualUserId: schedule.virtualUserId,
|
|
88
88
|
reason: 'dispatch-failed',
|
|
89
89
|
});
|
|
90
90
|
continue;
|
|
91
91
|
}
|
|
92
|
-
await schedules.claim(schedule.
|
|
92
|
+
await schedules.claim(schedule.virtualUserId, {
|
|
93
93
|
from: due,
|
|
94
94
|
nextRunAt: due,
|
|
95
95
|
runId,
|
|
96
96
|
at: now,
|
|
97
97
|
});
|
|
98
|
-
result.dispatched.push({
|
|
98
|
+
result.dispatched.push({ virtualUserId: schedule.virtualUserId, runId });
|
|
99
99
|
}
|
|
100
100
|
return result;
|
|
101
101
|
};
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ScenarioVirtualUser } from '../../services/virtual-users-service.js';
|
|
2
2
|
import type { VirtualUserTarget } from './virtual-user.types.js';
|
|
3
|
-
export interface
|
|
4
|
-
/** Model the
|
|
3
|
+
export interface VirtualUserTargetOptions {
|
|
4
|
+
/** Model the virtual user uses for its side of a conversation with an agent. */
|
|
5
5
|
model?: string;
|
|
6
6
|
/** Agents this user may talk to. Omit to leave `talkTo` unavailable. */
|
|
7
7
|
agents?: readonly string[];
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Drive a virtual user through
|
|
10
|
+
* Drive a virtual user's run through its signed-in session.
|
|
11
11
|
*
|
|
12
|
-
* This is the only place the engine meets the network. A
|
|
12
|
+
* This is the only place the engine meets the network. A virtual user already signs
|
|
13
13
|
* in as a real user over the app's own auth, keeps its cookies, re-logs-in when
|
|
14
14
|
* a session expires mid-run and reports a status as data rather than throwing —
|
|
15
15
|
* everything a user imitating a user needs, and none of it worth building twice.
|
|
@@ -18,4 +18,4 @@ export interface PersonaTargetOptions {
|
|
|
18
18
|
* error, it is the system saying no, which is information it should be allowed
|
|
19
19
|
* to act on.
|
|
20
20
|
*/
|
|
21
|
-
export declare const
|
|
21
|
+
export declare const virtualUserTarget: (virtualUser: ScenarioVirtualUser, { model, agents }?: VirtualUserTargetOptions) => VirtualUserTarget;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drive a virtual user through
|
|
2
|
+
* Drive a virtual user's run through its signed-in session.
|
|
3
3
|
*
|
|
4
|
-
* This is the only place the engine meets the network. A
|
|
4
|
+
* This is the only place the engine meets the network. A virtual user already signs
|
|
5
5
|
* in as a real user over the app's own auth, keeps its cookies, re-logs-in when
|
|
6
6
|
* a session expires mid-run and reports a status as data rather than throwing —
|
|
7
7
|
* everything a user imitating a user needs, and none of it worth building twice.
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* error, it is the system saying no, which is information it should be allowed
|
|
11
11
|
* to act on.
|
|
12
12
|
*/
|
|
13
|
-
export const
|
|
14
|
-
call: (rpcName, args) =>
|
|
13
|
+
export const virtualUserTarget = (virtualUser, { model, agents } = {}) => ({
|
|
14
|
+
call: (rpcName, args) => virtualUser.invokeRaw(rpcName, args),
|
|
15
15
|
talkTo: agents?.length
|
|
16
16
|
? async (agent, task) => {
|
|
17
17
|
if (!agents.includes(agent)) {
|
|
@@ -21,7 +21,7 @@ export const personaVirtualUserTarget = (persona, { model, agents } = {}) => ({
|
|
|
21
21
|
transcript: [],
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
return
|
|
24
|
+
return virtualUser.converse({
|
|
25
25
|
agent,
|
|
26
26
|
task,
|
|
27
27
|
// A user asking for something judges the answer by whether they got
|