@pikku/cli 0.12.111 → 0.12.113
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 +172 -0
- package/console-app/assets/{abnfDiagram-N423BO3Z-Bz2Ycfpe.js → abnfDiagram-N423BO3Z-Bltf5dmS.js} +1 -1
- package/console-app/assets/architecture-TIHT7OUA-DgRqRaqo.js +1 -0
- package/console-app/assets/{architectureDiagram-T3A2C74G-CvAkxhy0.js → architectureDiagram-T3A2C74G-BTtx6R7-.js} +1 -1
- package/console-app/assets/{blockDiagram-VBNYF7ZC-CuVOo2PB.js → blockDiagram-VBNYF7ZC-DgeTIOaM.js} +1 -1
- package/console-app/assets/{c4Diagram-5PPSVZJV-4Vg8SxCa.js → c4Diagram-5PPSVZJV-CaswnNob.js} +1 -1
- package/console-app/assets/channel-37lNznWk.js +1 -0
- package/console-app/assets/{chunk-2GRJ4B5K-CKKR4_dC.js → chunk-2GRJ4B5K-OnpnkpQT.js} +1 -1
- package/console-app/assets/{chunk-4I5QYGJK-C-m_kVjm.js → chunk-4I5QYGJK-CWmfNXFr.js} +1 -1
- package/console-app/assets/{chunk-5RXB4S5H-CyUGtd0h.js → chunk-5RXB4S5H-M8QmKeuk.js} +1 -1
- package/console-app/assets/{chunk-6Q2QTUOP-BuzItInf.js → chunk-6Q2QTUOP-C_h5lS68.js} +1 -1
- package/console-app/assets/{chunk-7Z6QIM7H-BGiBPuWt.js → chunk-7Z6QIM7H-DSnVb09k.js} +1 -1
- package/console-app/assets/{chunk-GF5L2VYU-D8jaliB9.js → chunk-GF5L2VYU-BkX038sq.js} +1 -1
- package/console-app/assets/{chunk-I66GZJ75-C-Lq_UXl.js → chunk-I66GZJ75-b3yMIljX.js} +1 -1
- package/console-app/assets/{chunk-JQJVKLGR-D7viK-tf.js → chunk-JQJVKLGR-Bihcz4nv.js} +1 -1
- package/console-app/assets/{chunk-KBJHAD2P-BS9N9gG4.js → chunk-KBJHAD2P-CS7eflbk.js} +1 -1
- package/console-app/assets/{chunk-NSK5VX7P-Fi_H0vaS.js → chunk-NSK5VX7P-CfigFGDQ.js} +1 -1
- package/console-app/assets/{chunk-QR6OTTB3-BJdg-tcL.js → chunk-QR6OTTB3-BBy8NfTW.js} +1 -1
- package/console-app/assets/{chunk-RYQCIY6F-CVbGA6YZ.js → chunk-RYQCIY6F-BCfZVQef.js} +1 -1
- package/console-app/assets/{chunk-UBXNYLIW-BF-sAqEK.js → chunk-UBXNYLIW-DkMKDHW4.js} +1 -1
- package/console-app/assets/{chunk-W5SLKNZC-CXrJJ0BX.js → chunk-W5SLKNZC-BSLytTSJ.js} +1 -1
- package/console-app/assets/{chunk-WRU74C26-CGzysGBy.js → chunk-WRU74C26-Dic7ZP8H.js} +1 -1
- package/console-app/assets/chunk-XXDRQBXY-B1prMCvW.js +1 -0
- package/console-app/assets/classDiagram-JCYQIIEL-BDCX9ZLm.js +1 -0
- package/console-app/assets/classDiagram-v2-OCEON4UE-BDCX9ZLm.js +1 -0
- package/console-app/assets/{cose-bilkent-JH36ORCC-BvFZIDHb.js → cose-bilkent-JH36ORCC-CEpWcE3M.js} +1 -1
- package/console-app/assets/{cynefin-VYW2F7L2-DRQ8xGmN.js → cynefin-VYW2F7L2-irPiPhgz.js} +1 -1
- package/console-app/assets/{cynefinDiagram-MW4NZA55-BwxQ4C1z.js → cynefinDiagram-MW4NZA55-CIkqQ912.js} +1 -1
- package/console-app/assets/{dagre-J22zmNeE.js → dagre-FZyyw3H3.js} +1 -1
- package/console-app/assets/{dagre-VZM6K2ZE-DPtUyvjD.js → dagre-VZM6K2ZE-Dxnnu7Rh.js} +1 -1
- package/console-app/assets/{diagram-7IWD3JNH-DGAzJ2Ln.js → diagram-7IWD3JNH-DaT47sVs.js} +1 -1
- package/console-app/assets/{diagram-B4RE2ZJO-D14cIfto.js → diagram-B4RE2ZJO-Dmht03tx.js} +1 -1
- package/console-app/assets/{diagram-LBJQPF4R-CUscJ_pO.js → diagram-LBJQPF4R-B--jwILh.js} +1 -1
- package/console-app/assets/{diagram-Q27KOJAE-CehBHMt3.js → diagram-Q27KOJAE-D9H0euUr.js} +1 -1
- package/console-app/assets/{diagram-UB23O5K3-BvazI_qH.js → diagram-UB23O5K3-DloUQ8tO.js} +1 -1
- package/console-app/assets/{ebnfDiagram-BXEA7PRR-CBZPYFwZ.js → ebnfDiagram-BXEA7PRR-gaOXzNBU.js} +1 -1
- package/console-app/assets/{erDiagram-JOGREHBK-BN3FevvT.js → erDiagram-JOGREHBK-3X9eTLsh.js} +1 -1
- package/console-app/assets/eventmodeling-45OFAUF4-BarrKD55.js +1 -0
- package/console-app/assets/flowDiagram-UKHOOZJN-B6RiRjcD.js +1 -0
- package/console-app/assets/{ganttDiagram-PKOTCBZU-03mWH2kB.js → ganttDiagram-PKOTCBZU-khAFwSBy.js} +1 -1
- package/console-app/assets/{gitGraph-TEB2WS4Q-By2486wn.js → gitGraph-TEB2WS4Q-Bb7uENmk.js} +1 -1
- package/console-app/assets/{gitGraphDiagram-DS77QQ5N-CCzKPRBJ.js → gitGraphDiagram-DS77QQ5N-OkSBM0h0.js} +1 -1
- package/console-app/assets/{index-snBzz_U8.js → index-Bn1BoT3w.js} +184 -184
- package/console-app/assets/{info-DKCQHKI2-C_vsavqC.js → info-DKCQHKI2-BH5dbX2B.js} +1 -1
- package/console-app/assets/{infoDiagram-6WML65LV-BKvU8du7.js → infoDiagram-6WML65LV-IbUl8J2C.js} +1 -1
- package/console-app/assets/{ishikawaDiagram-WSZJBQD7-CnF0qm0n.js → ishikawaDiagram-WSZJBQD7-CX0gA3de.js} +1 -1
- package/console-app/assets/{journeyDiagram-NVQOT4AX-DxKe9OZ7.js → journeyDiagram-NVQOT4AX--k2etXu8.js} +1 -1
- package/console-app/assets/{kanban-definition-27J2QSJJ-CB8lgPxU.js → kanban-definition-27J2QSJJ-D1FvtPpN.js} +1 -1
- package/console-app/assets/{line-DFNLso1I.js → line-JLMY_DDK.js} +1 -1
- package/console-app/assets/{linear-D1cspVtk.js → linear-CSvLB3cd.js} +1 -1
- package/console-app/assets/{mermaid-parser.core-BtmN233C.js → mermaid-parser.core-CwwjcAjz.js} +3 -3
- package/console-app/assets/{mermaid.core-37H5_HoB.js → mermaid.core-hmS_M4K5.js} +4 -4
- package/console-app/assets/{mindmap-definition-FAOFIHXS-Czf82JOB.js → mindmap-definition-FAOFIHXS-rTDa_CTp.js} +1 -1
- package/console-app/assets/{packet-7NZHBO7P-CErYxAQM.js → packet-7NZHBO7P-DBUJcYfI.js} +1 -1
- package/console-app/assets/{pegDiagram-VL7TDLO6-CRKX6HOt.js → pegDiagram-VL7TDLO6-D42PxXnS.js} +1 -1
- package/console-app/assets/{pie-RZYD4A2V-YmMmh6Nq.js → pie-RZYD4A2V-dM0uFAE4.js} +1 -1
- package/console-app/assets/{pieDiagram-7S7Q4E2Y-Cot0Fu5q.js → pieDiagram-7S7Q4E2Y-wCln1ucl.js} +1 -1
- package/console-app/assets/{quadrantDiagram-CIZ2JOQS-DPEonVYT.js → quadrantDiagram-CIZ2JOQS-DrRwS1g5.js} +1 -1
- package/console-app/assets/{radar-I7S5WNFK-Pf_KiQJM.js → radar-I7S5WNFK-BW0V-EGR.js} +1 -1
- package/console-app/assets/{railroad-3IZDKUUU-BYoOMoQY.js → railroad-3IZDKUUU-FiTHaDbb.js} +1 -1
- package/console-app/assets/railroad-abnf-AHOZXSZD-lmUjOixE.js +1 -0
- package/console-app/assets/railroad-ebnf-EBAXGLYW-ZjdA7OHp.js +1 -0
- package/console-app/assets/railroad-peg-LSFZ7HO6-VHSlvwI8.js +1 -0
- package/console-app/assets/{railroadDiagram-AXF67PYL-Di8N9tHD.js → railroadDiagram-AXF67PYL-9ef7j5U7.js} +1 -1
- package/console-app/assets/{requirementDiagram-LRYGKXZP-2yZZ753q.js → requirementDiagram-LRYGKXZP-Cu90DbEZ.js} +1 -1
- package/console-app/assets/{sankeyDiagram-W5VNT64P-BwYA-xq1.js → sankeyDiagram-W5VNT64P-Bhxmyxaw.js} +1 -1
- package/console-app/assets/{sequenceDiagram-SI44F4Z6-odIxlx8J.js → sequenceDiagram-SI44F4Z6-CBsbRTz8.js} +1 -1
- package/console-app/assets/{src-CxJwpjvz.js → src-BR8qR3lM.js} +1 -1
- package/console-app/assets/{stateDiagram-OKZ733FA-Du2S0qU9.js → stateDiagram-OKZ733FA-Dr0frETZ.js} +1 -1
- package/console-app/assets/stateDiagram-v2-UEYNNEHI-BSXWhCvf.js +1 -0
- package/console-app/assets/{swimlanes-SLNWSIFB-JZ3swzI2.js → swimlanes-SLNWSIFB-CXMjvd_a.js} +1 -1
- package/console-app/assets/swimlanesDiagram-ULZ7WXOC-BUdsPDKC.js +8 -0
- package/console-app/assets/{timeline-definition-Z64GVDOM-CsppAEWi.js → timeline-definition-Z64GVDOM-DpmEZPV0.js} +1 -1
- package/console-app/assets/{treeView-QDETBFTQ-DkkO8peQ.js → treeView-QDETBFTQ-CjmuinMk.js} +1 -1
- package/console-app/assets/{treemap-6X3UGDF4-hjLkPCJI.js → treemap-6X3UGDF4-BP_fU6Cn.js} +1 -1
- package/console-app/assets/{vennDiagram-T6HMQDX7-BOfgM1xp.js → vennDiagram-T6HMQDX7-DgZS7iuW.js} +1 -1
- package/console-app/assets/{wardley-OPB4EBWU-C5ZtGfBa.js → wardley-OPB4EBWU-CgMPoouN.js} +1 -1
- package/console-app/assets/{wardleyDiagram-T6FBY63Y-ymf3Vm6A.js → wardleyDiagram-T6FBY63Y-6y-xxP1R.js} +1 -1
- package/console-app/assets/{xychartDiagram-ELKLHX3M-DdxXIoKz.js → xychartDiagram-ELKLHX3M-DOeIQjYI.js} +1 -1
- package/console-app/index.html +1 -1
- package/dist/.pikku/addon/index.d.ts +1 -1
- package/dist/.pikku/addon/index.js +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.js +1 -1
- package/dist/.pikku/agent/index.d.ts +1 -1
- package/dist/.pikku/agent/index.js +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/agent/pikku-model-aliases.gen.js +1 -1
- package/dist/.pikku/auth/index.d.ts +1 -1
- package/dist/.pikku/auth/index.js +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.d.ts +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.js +1 -1
- package/dist/.pikku/channel/index.d.ts +1 -1
- package/dist/.pikku/channel/index.js +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/index.d.ts +1 -1
- package/dist/.pikku/cli/index.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/error/index.d.ts +1 -1
- package/dist/.pikku/error/index.js +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.d.ts +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.js +1 -1
- package/dist/.pikku/function/index.d.ts +1 -1
- package/dist/.pikku/function/index.js +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +3 -3
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/gateway/index.d.ts +1 -1
- package/dist/.pikku/gateway/index.js +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.d.ts +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.js +1 -1
- package/dist/.pikku/http/index.d.ts +1 -1
- package/dist/.pikku/http/index.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/mcp/index.d.ts +1 -1
- package/dist/.pikku/mcp/index.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/middleware/index.d.ts +1 -1
- package/dist/.pikku/middleware/index.js +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.d.ts +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +2 -1
- package/dist/.pikku/pikku-services.gen.js +1 -0
- package/dist/.pikku/queue/index.d.ts +1 -1
- package/dist/.pikku/queue/index.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/scenarios/index.d.ts +1 -1
- package/dist/.pikku/scenarios/index.js +1 -1
- package/dist/.pikku/scenarios/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-types.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.js +1 -1
- package/dist/.pikku/scheduler/index.d.ts +1 -1
- package/dist/.pikku/scheduler/index.js +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +1 -1
- package/dist/.pikku/scopes/index.d.ts +1 -1
- package/dist/.pikku/scopes/index.js +1 -1
- package/dist/.pikku/scopes/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-roles.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/secrets/index.d.ts +1 -1
- package/dist/.pikku/secrets/index.js +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/services/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/services/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/setup/index.d.ts +1 -1
- package/dist/.pikku/setup/index.js +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.d.ts +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.js +1 -1
- package/dist/.pikku/trigger/index.d.ts +1 -1
- package/dist/.pikku/trigger/index.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/index.d.ts +1 -1
- package/dist/.pikku/variables/index.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/index.d.ts +1 -1
- package/dist/.pikku/workflow/index.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/functions/commands/dev.js +72 -62
- package/dist/src/functions/commands/persona.js +6 -0
- package/dist/src/functions/commands/watch.js +56 -37
- package/dist/src/functions/wirings/virtual-user/serialize-virtual-user-functions.js +485 -102
- package/package.json +4 -4
- package/surface.json +1 -1
- package/console-app/assets/architecture-TIHT7OUA-5QhJKy4P.js +0 -1
- package/console-app/assets/channel-DoE3nnYB.js +0 -1
- package/console-app/assets/chunk-XXDRQBXY-BDALKVU4.js +0 -1
- package/console-app/assets/classDiagram-JCYQIIEL-BgrrHQtN.js +0 -1
- package/console-app/assets/classDiagram-v2-OCEON4UE-BgrrHQtN.js +0 -1
- package/console-app/assets/eventmodeling-45OFAUF4-D2SxzJUl.js +0 -1
- package/console-app/assets/flowDiagram-UKHOOZJN-CQXuJ0z2.js +0 -1
- package/console-app/assets/railroad-abnf-AHOZXSZD-CCRXsRRD.js +0 -1
- package/console-app/assets/railroad-ebnf-EBAXGLYW-DVRSVgn8.js +0 -1
- package/console-app/assets/railroad-peg-LSFZ7HO6-COUBoNH3.js +0 -1
- package/console-app/assets/stateDiagram-v2-UEYNNEHI-CTXOGDRR.js +0 -1
- package/console-app/assets/swimlanesDiagram-ULZ7WXOC-DRrggQgK.js +0 -8
|
@@ -39,6 +39,16 @@ export const Disposition = z.enum([
|
|
|
39
39
|
'accountable',
|
|
40
40
|
])
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Where a run stops. Not a schedule: these cap one outing, and how often the
|
|
44
|
+
* outings happen is \`setVirtualUserSchedule\`'s business.
|
|
45
|
+
*/
|
|
46
|
+
export const Budget = z.object({
|
|
47
|
+
steps: z.number().int().min(1).max(500).optional(),
|
|
48
|
+
mutations: z.number().int().min(0).max(500).optional(),
|
|
49
|
+
durationMs: z.number().int().min(1000).max(3_600_000).optional(),
|
|
50
|
+
})
|
|
51
|
+
|
|
42
52
|
export const RunVirtualUserInput = z.object({
|
|
43
53
|
/** A persona id from \`definePersonas()\`. */
|
|
44
54
|
persona: z.string().min(1),
|
|
@@ -56,13 +66,7 @@ export const RunVirtualUserInput = z.object({
|
|
|
56
66
|
memory: z.record(z.string(), z.string()).optional(),
|
|
57
67
|
/** Overrides the persona's declared disposition for this run only. */
|
|
58
68
|
disposition: Disposition.optional(),
|
|
59
|
-
budget:
|
|
60
|
-
.object({
|
|
61
|
-
steps: z.number().int().min(1).max(500).optional(),
|
|
62
|
-
mutations: z.number().int().min(0).max(500).optional(),
|
|
63
|
-
durationMs: z.number().int().min(1000).max(3_600_000).optional(),
|
|
64
|
-
})
|
|
65
|
-
.optional(),
|
|
69
|
+
budget: Budget.optional(),
|
|
66
70
|
/** Fixed seed, so a run replays into the same finding. */
|
|
67
71
|
seed: z.number().int().optional(),
|
|
68
72
|
})
|
|
@@ -90,7 +94,45 @@ export const Finding = z.object({
|
|
|
90
94
|
step: z.number(),
|
|
91
95
|
})
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
/**
|
|
98
|
+
* What the user set out to do, and how far each one got. The spine a transcript
|
|
99
|
+
* hangs off — the steps alone are a list of calls with no account of what they
|
|
100
|
+
* were for.
|
|
101
|
+
*/
|
|
102
|
+
export const Intent = z.object({
|
|
103
|
+
id: z.string(),
|
|
104
|
+
sourceId: z.string(),
|
|
105
|
+
title: z.string(),
|
|
106
|
+
/** open | suspended | completed | abandoned | stuck, open-ended for the same
|
|
107
|
+
* reason a finding's \`kind\` is. */
|
|
108
|
+
status: z.string(),
|
|
109
|
+
/** Step indices at which this intent was the active one. */
|
|
110
|
+
steps: z.array(z.number()),
|
|
111
|
+
/** How many times it was put down and picked back up. */
|
|
112
|
+
suspensions: z.number(),
|
|
113
|
+
summary: z.string().optional(),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/** One turn: what the engine scheduled, what the model did, what came back. */
|
|
117
|
+
export const Step = z.object({
|
|
118
|
+
index: z.number(),
|
|
119
|
+
intentId: z.string().optional(),
|
|
120
|
+
/** The call it chose, or the \`invalid\` shape for a turn it got wrong. */
|
|
121
|
+
action: z.record(z.string(), z.unknown()),
|
|
122
|
+
status: z.number().optional(),
|
|
123
|
+
ok: z.boolean().optional(),
|
|
124
|
+
/** Truncated by the engine, so a transcript stays readable. */
|
|
125
|
+
response: z.string().optional(),
|
|
126
|
+
findingKinds: z.array(z.string()).optional(),
|
|
127
|
+
tokensIn: z.number(),
|
|
128
|
+
tokensOut: z.number(),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Shared so a run reads the same whether it arrived one at a time or in a list.
|
|
133
|
+
* Not exported: the schemas the inspector names are the ones a function wires.
|
|
134
|
+
*/
|
|
135
|
+
const virtualUserRunFields = {
|
|
94
136
|
runId: z.string(),
|
|
95
137
|
persona: z.string(),
|
|
96
138
|
disposition: Disposition,
|
|
@@ -99,12 +141,77 @@ export const GetVirtualUserRunOutput = z.object({
|
|
|
99
141
|
goals: z.array(z.string()),
|
|
100
142
|
memory: z.record(z.string(), z.string()),
|
|
101
143
|
findings: z.array(Finding),
|
|
144
|
+
intents: z.array(Intent),
|
|
102
145
|
/** Steps, calls, mutations, tokens and elapsed time, as the engine counted them. */
|
|
103
146
|
tally: z.record(z.string(), z.unknown()).nullable(),
|
|
104
147
|
stoppedBy: z.string().nullable(),
|
|
105
148
|
error: z.string().nullable(),
|
|
106
149
|
createdAt: z.string(),
|
|
107
150
|
finishedAt: z.string().nullable(),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const GetVirtualUserRunOutput = z.object(virtualUserRunFields)
|
|
154
|
+
|
|
155
|
+
export const ListVirtualUserRunsInput = z.object({
|
|
156
|
+
/** Narrows to one persona's history. */
|
|
157
|
+
persona: z.string().min(1).optional(),
|
|
158
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
159
|
+
offset: z.number().int().min(0).optional(),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
export const ListVirtualUserRunsOutput = z.object({
|
|
163
|
+
/** Newest first. The transcript is not here — read it with getVirtualUserRunSteps. */
|
|
164
|
+
runs: z.array(z.object(virtualUserRunFields)),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
export const GetVirtualUserRunStepsInput = z.object({
|
|
168
|
+
runId: z.string(),
|
|
169
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
170
|
+
offset: z.number().int().min(0).optional(),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
export const GetVirtualUserRunStepsOutput = z.object({
|
|
174
|
+
steps: z.array(Step),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* One persona's standing instruction to keep using the app.
|
|
179
|
+
*
|
|
180
|
+
* The interval is a range rather than a number: a user who arrives at exactly
|
|
181
|
+
* 09:00 every day exercises one cache state and one cron neighbourhood, and a
|
|
182
|
+
* real one does not keep an appointment.
|
|
183
|
+
*/
|
|
184
|
+
export const SetVirtualUserScheduleInput = z.object({
|
|
185
|
+
persona: z.string().min(1),
|
|
186
|
+
/** Off until something says otherwise — a schedule costs money on every tick. */
|
|
187
|
+
enabled: z.boolean().optional(),
|
|
188
|
+
disposition: Disposition.optional(),
|
|
189
|
+
goals: z.array(z.string().min(1)).max(20).optional(),
|
|
190
|
+
budget: Budget.nullable().optional(),
|
|
191
|
+
minIntervalMs: z.number().int().min(60_000).optional(),
|
|
192
|
+
maxIntervalMs: z.number().int().min(60_000).optional(),
|
|
193
|
+
/** ISO. Omitted on a new schedule means the persona is due at once. */
|
|
194
|
+
nextRunAt: z.string().optional(),
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
/** Not exported for the same reason the run fields are not. */
|
|
198
|
+
const virtualUserScheduleFields = {
|
|
199
|
+
persona: z.string(),
|
|
200
|
+
enabled: z.boolean(),
|
|
201
|
+
disposition: Disposition,
|
|
202
|
+
goals: z.array(z.string()),
|
|
203
|
+
budget: Budget.nullable(),
|
|
204
|
+
minIntervalMs: z.number(),
|
|
205
|
+
maxIntervalMs: z.number(),
|
|
206
|
+
nextRunAt: z.string(),
|
|
207
|
+
lastRunId: z.string().nullable(),
|
|
208
|
+
lastRunAt: z.string().nullable(),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const SetVirtualUserScheduleOutput = z.object(virtualUserScheduleFields)
|
|
212
|
+
|
|
213
|
+
export const ListVirtualUserSchedulesOutput = z.object({
|
|
214
|
+
schedules: z.array(z.object(virtualUserScheduleFields)),
|
|
108
215
|
})
|
|
109
216
|
|
|
110
217
|
/** The internal dispatch payload — everything the run was resolved down to. */
|
|
@@ -138,6 +245,10 @@ import { personaVirtualUserTarget, runVirtualUser as runVirtualUserEngine, type
|
|
|
138
245
|
import {
|
|
139
246
|
prepareVirtualUserRun,
|
|
140
247
|
PRODUCTION_DISPOSITION,
|
|
248
|
+
tickVirtualUserSchedules as tick,
|
|
249
|
+
type VirtualUserRunRecord,
|
|
250
|
+
type VirtualUserRunStore,
|
|
251
|
+
type VirtualUserScheduleRecord,
|
|
141
252
|
} from '@pikku/core/virtual-user'
|
|
142
253
|
import { createPersonas, personaConfigs } from '${pathToPersonas}'
|
|
143
254
|
import {
|
|
@@ -145,8 +256,15 @@ import {
|
|
|
145
256
|
ExecuteVirtualUserRunOutput,
|
|
146
257
|
GetVirtualUserRunInput,
|
|
147
258
|
GetVirtualUserRunOutput,
|
|
259
|
+
GetVirtualUserRunStepsInput,
|
|
260
|
+
GetVirtualUserRunStepsOutput,
|
|
261
|
+
ListVirtualUserRunsInput,
|
|
262
|
+
ListVirtualUserRunsOutput,
|
|
263
|
+
ListVirtualUserSchedulesOutput,
|
|
148
264
|
RunVirtualUserInput,
|
|
149
265
|
RunVirtualUserOutput,
|
|
266
|
+
SetVirtualUserScheduleInput,
|
|
267
|
+
SetVirtualUserScheduleOutput,
|
|
150
268
|
} from './virtual-user.schemas.gen.js'
|
|
151
269
|
|
|
152
270
|
defineScope({
|
|
@@ -156,6 +274,9 @@ defineScope({
|
|
|
156
274
|
scopes: {
|
|
157
275
|
run: { description: 'Start a virtual user run' },
|
|
158
276
|
read: { description: "Read a run's findings" },
|
|
277
|
+
schedule: {
|
|
278
|
+
description: 'Decide that a persona keeps running on its own',
|
|
279
|
+
},
|
|
159
280
|
},
|
|
160
281
|
},
|
|
161
282
|
})
|
|
@@ -170,89 +291,156 @@ const API_URL_VARIABLE = 'VIRTUAL_USER_API_URL'
|
|
|
170
291
|
const SECRET_VARIABLE = 'SCENARIO_ACTOR_SECRET'
|
|
171
292
|
const MODEL_VARIABLE = 'VIRTUAL_USER_MODEL'
|
|
172
293
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
294
|
+
/**
|
|
295
|
+
* One run on the wire. Findings and intents are free-form by design — the
|
|
296
|
+
* engine records what it noticed, not a fixed row shape — so they cross as the
|
|
297
|
+
* schema's open objects rather than being narrowed to whatever kinds exist
|
|
298
|
+
* today.
|
|
299
|
+
*/
|
|
300
|
+
const serializeRun = (run: VirtualUserRunRecord) => ({
|
|
301
|
+
runId: run.runId,
|
|
302
|
+
persona: run.persona,
|
|
303
|
+
disposition: run.disposition,
|
|
304
|
+
seed: run.seed,
|
|
305
|
+
status: run.status,
|
|
306
|
+
goals: run.goals,
|
|
307
|
+
memory: run.memory,
|
|
308
|
+
findings: run.findings.map((finding) => ({
|
|
309
|
+
kind: finding.kind as string,
|
|
310
|
+
detail: finding.detail,
|
|
311
|
+
rpcName: finding.rpcName,
|
|
312
|
+
status: finding.status,
|
|
313
|
+
intentId: finding.intentId,
|
|
314
|
+
step: finding.step,
|
|
315
|
+
})),
|
|
316
|
+
intents: run.intents.map((intent) => ({
|
|
317
|
+
id: intent.id,
|
|
318
|
+
sourceId: intent.sourceId,
|
|
319
|
+
title: intent.title,
|
|
320
|
+
status: intent.status as string,
|
|
321
|
+
steps: intent.steps,
|
|
322
|
+
suspensions: intent.suspensions,
|
|
323
|
+
summary: intent.summary,
|
|
324
|
+
})),
|
|
325
|
+
tally: (run.tally ?? null) as Record<string, unknown> | null,
|
|
326
|
+
stoppedBy: run.stoppedBy,
|
|
327
|
+
error: run.error,
|
|
328
|
+
createdAt: run.createdAt.toISOString(),
|
|
329
|
+
finishedAt: run.finishedAt ? run.finishedAt.toISOString() : null,
|
|
330
|
+
})
|
|
193
331
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
332
|
+
/**
|
|
333
|
+
* Resolves a persona down to a run and kicks it off, for the two callers that
|
|
334
|
+
* start one: a person with the scope, and the tick acting on a schedule.
|
|
335
|
+
*
|
|
336
|
+
* Shared rather than duplicated because the checks are the point — an acted-upon
|
|
337
|
+
* persona has no session, and only one disposition may ever touch production.
|
|
338
|
+
* A second copy of that would eventually disagree with this one.
|
|
339
|
+
*/
|
|
340
|
+
const startVirtualUserRun = async (
|
|
341
|
+
services: {
|
|
342
|
+
virtualUserRunStore?: VirtualUserRunStore
|
|
343
|
+
config: { nodeEnv?: string }
|
|
344
|
+
},
|
|
345
|
+
rpc: { invoke: (name: string, data: any) => Promise<unknown> },
|
|
346
|
+
input: {
|
|
347
|
+
persona: string
|
|
348
|
+
disposition?: string
|
|
349
|
+
goals?: string[]
|
|
350
|
+
memory?: Record<string, string>
|
|
351
|
+
budget?: { steps?: number; mutations?: number; durationMs?: number }
|
|
352
|
+
seed?: number
|
|
353
|
+
startedBy?: string | null
|
|
354
|
+
}
|
|
355
|
+
): Promise<string> => {
|
|
356
|
+
const { virtualUserRunStore, config } = services
|
|
357
|
+
if (!virtualUserRunStore) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
'No virtualUserRunStore is wired — a run has nowhere to be recorded. ' +
|
|
360
|
+
'Wire KyselyVirtualUserRunStore from @pikku/kysely, or your own implementation of VirtualUserRunStore.'
|
|
361
|
+
)
|
|
362
|
+
}
|
|
209
363
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
364
|
+
const persona = personaConfigs[input.persona as keyof typeof personaConfigs] as
|
|
365
|
+
| { id: string; runnable: boolean; disposition?: string }
|
|
366
|
+
| undefined
|
|
367
|
+
if (!persona) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
\`Unknown persona "\${input.persona}" — declare it with definePersonas()\`
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
// An acted-upon persona has no session of its own, and running one would
|
|
373
|
+
// race whatever scenario acts on it.
|
|
374
|
+
if (!persona.runnable) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
\`Persona "\${input.persona}" is declared as acted upon, never run\`
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const disposition = (input.disposition ??
|
|
381
|
+
persona.disposition ??
|
|
382
|
+
'realistic') as VirtualUserDisposition
|
|
383
|
+
// Every disposition other than this one exists to find out what the product
|
|
384
|
+
// does wrong, which is not a thing to do to real customers' data. Checked
|
|
385
|
+
// against the effective disposition, so the override cannot smuggle one in.
|
|
386
|
+
if (
|
|
387
|
+
config.nodeEnv === 'production' &&
|
|
388
|
+
disposition !== PRODUCTION_DISPOSITION
|
|
389
|
+
) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
\`Only the '\${PRODUCTION_DISPOSITION}' disposition may run against production; "\${input.persona}" is \${disposition}\`
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Seeded here rather than inside the engine so the record carries the seed
|
|
396
|
+
// even if the run dies before returning — an unreproducible crash costs the
|
|
397
|
+
// most.
|
|
398
|
+
const seed = input.seed ?? Math.floor(Math.random() * 2_147_483_647)
|
|
224
399
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
400
|
+
const runId = await virtualUserRunStore.start({
|
|
401
|
+
persona: persona.id,
|
|
402
|
+
disposition,
|
|
403
|
+
seed,
|
|
404
|
+
goals: input.goals ?? [],
|
|
405
|
+
memory: input.memory ?? {},
|
|
406
|
+
startedBy: input.startedBy ?? null,
|
|
407
|
+
})
|
|
229
408
|
|
|
230
|
-
|
|
409
|
+
// Deliberately not awaited: a run takes minutes, and a held-open request
|
|
410
|
+
// survives neither a rollout nor a proxy timeout. executeVirtualUserRun
|
|
411
|
+
// writes both outcomes to the record itself, so the only thing left to
|
|
412
|
+
// handle here is a rejection escaping the promise — without the catch it
|
|
413
|
+
// becomes an unhandled rejection and takes the process with it.
|
|
414
|
+
void rpc
|
|
415
|
+
.invoke('executeVirtualUserRun', {
|
|
416
|
+
runId,
|
|
231
417
|
persona: persona.id,
|
|
232
418
|
disposition,
|
|
233
|
-
seed,
|
|
234
419
|
goals: input.goals ?? [],
|
|
235
420
|
memory: input.memory ?? {},
|
|
236
|
-
|
|
421
|
+
budget: input.budget,
|
|
422
|
+
seed,
|
|
237
423
|
})
|
|
424
|
+
.catch(() => {})
|
|
238
425
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// writes both outcomes to the record itself, so the only thing left to
|
|
242
|
-
// handle here is a rejection escaping the promise — without the catch it
|
|
243
|
-
// becomes an unhandled rejection and takes the process with it.
|
|
244
|
-
void rpc!
|
|
245
|
-
.invoke('executeVirtualUserRun', {
|
|
246
|
-
runId,
|
|
247
|
-
persona: persona.id,
|
|
248
|
-
disposition,
|
|
249
|
-
goals: input.goals ?? [],
|
|
250
|
-
memory: input.memory ?? {},
|
|
251
|
-
budget: input.budget,
|
|
252
|
-
seed,
|
|
253
|
-
})
|
|
254
|
-
.catch(() => {})
|
|
426
|
+
return runId
|
|
427
|
+
}
|
|
255
428
|
|
|
429
|
+
export const runVirtualUser = pikkuFunc({
|
|
430
|
+
tags: ['pikku'],
|
|
431
|
+
title: 'Run a Virtual User',
|
|
432
|
+
description:
|
|
433
|
+
'Turns a declared persona loose on this application and records what it finds. Returns immediately with a run id; read the result back with getVirtualUserRun.',
|
|
434
|
+
expose: true,
|
|
435
|
+
scopes: ['virtualUser:run'],
|
|
436
|
+
input: RunVirtualUserInput,
|
|
437
|
+
output: RunVirtualUserOutput,
|
|
438
|
+
func: async ({ virtualUserRunStore, config }, input, { session, rpc }) => {
|
|
439
|
+
const runId = await startVirtualUserRun(
|
|
440
|
+
{ virtualUserRunStore, config },
|
|
441
|
+
rpc!,
|
|
442
|
+
{ ...input, startedBy: session?.userId ?? null }
|
|
443
|
+
)
|
|
256
444
|
return { runId }
|
|
257
445
|
},
|
|
258
446
|
})
|
|
@@ -277,30 +465,223 @@ export const getVirtualUserRun = pikkuFunc({
|
|
|
277
465
|
if (!run) {
|
|
278
466
|
throw new Error(\`No virtual user run \${runId}\`)
|
|
279
467
|
}
|
|
468
|
+
return serializeRun(run)
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
export const listVirtualUserRuns = pikkuFunc({
|
|
473
|
+
tags: ['pikku'],
|
|
474
|
+
title: 'List Virtual User Runs',
|
|
475
|
+
description:
|
|
476
|
+
"Reads back what the virtual users have been doing, newest first. Narrow with \`persona\` for one user's history.",
|
|
477
|
+
expose: true,
|
|
478
|
+
scopes: ['virtualUser:read'],
|
|
479
|
+
input: ListVirtualUserRunsInput,
|
|
480
|
+
output: ListVirtualUserRunsOutput,
|
|
481
|
+
func: async ({ virtualUserRunStore }, { persona, limit, offset }) => {
|
|
482
|
+
if (!virtualUserRunStore) {
|
|
483
|
+
throw new Error('No virtualUserRunStore is wired — there are no runs to read.')
|
|
484
|
+
}
|
|
485
|
+
const runs = await virtualUserRunStore.list({ persona, limit, offset })
|
|
486
|
+
return { runs: runs.map(serializeRun) }
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
export const getVirtualUserRunSteps = pikkuFunc({
|
|
491
|
+
tags: ['pikku'],
|
|
492
|
+
title: 'Read a Virtual User Transcript',
|
|
493
|
+
description:
|
|
494
|
+
'Every turn one run took, in order: what it called, what came back, and what that cost.',
|
|
495
|
+
expose: true,
|
|
496
|
+
// The transcript is strictly more sensitive than the summary it belongs to —
|
|
497
|
+
// it carries the live ids and payloads the run actually sent — so it sits
|
|
498
|
+
// behind the same read scope rather than a looser one.
|
|
499
|
+
scopes: ['virtualUser:read'],
|
|
500
|
+
input: GetVirtualUserRunStepsInput,
|
|
501
|
+
output: GetVirtualUserRunStepsOutput,
|
|
502
|
+
func: async ({ virtualUserRunStore }, { runId, limit, offset }) => {
|
|
503
|
+
if (!virtualUserRunStore) {
|
|
504
|
+
throw new Error('No virtualUserRunStore is wired — there are no runs to read.')
|
|
505
|
+
}
|
|
506
|
+
const steps = await virtualUserRunStore.steps(runId, { limit, offset })
|
|
280
507
|
return {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
findings: run.findings.map((finding) => ({
|
|
292
|
-
kind: finding.kind as string,
|
|
293
|
-
detail: finding.detail,
|
|
294
|
-
rpcName: finding.rpcName,
|
|
295
|
-
status: finding.status,
|
|
296
|
-
intentId: finding.intentId,
|
|
297
|
-
step: finding.step,
|
|
508
|
+
steps: steps.map((step) => ({
|
|
509
|
+
index: step.index,
|
|
510
|
+
intentId: step.intentId,
|
|
511
|
+
action: step.action as unknown as Record<string, unknown>,
|
|
512
|
+
status: step.status,
|
|
513
|
+
ok: step.ok,
|
|
514
|
+
response: step.response,
|
|
515
|
+
findingKinds: step.findingKinds as string[] | undefined,
|
|
516
|
+
tokensIn: step.tokensIn,
|
|
517
|
+
tokensOut: step.tokensOut,
|
|
298
518
|
})),
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* One schedule on the wire. The budget crosses as \`durationMs\` because that is
|
|
525
|
+
* what every other call here takes; the engine's own duration also accepts
|
|
526
|
+
* \`'30m'\`, which nothing on this side ever writes.
|
|
527
|
+
*/
|
|
528
|
+
const serializeSchedule = (schedule: VirtualUserScheduleRecord) => ({
|
|
529
|
+
persona: schedule.persona,
|
|
530
|
+
enabled: schedule.enabled,
|
|
531
|
+
disposition: schedule.disposition,
|
|
532
|
+
goals: schedule.goals,
|
|
533
|
+
budget: schedule.budget
|
|
534
|
+
? {
|
|
535
|
+
steps: schedule.budget.steps,
|
|
536
|
+
mutations: schedule.budget.mutations,
|
|
537
|
+
durationMs:
|
|
538
|
+
typeof schedule.budget.duration === 'number'
|
|
539
|
+
? schedule.budget.duration
|
|
540
|
+
: undefined,
|
|
541
|
+
}
|
|
542
|
+
: null,
|
|
543
|
+
minIntervalMs: schedule.minIntervalMs,
|
|
544
|
+
maxIntervalMs: schedule.maxIntervalMs,
|
|
545
|
+
nextRunAt: schedule.nextRunAt.toISOString(),
|
|
546
|
+
lastRunId: schedule.lastRunId,
|
|
547
|
+
lastRunAt: schedule.lastRunAt ? schedule.lastRunAt.toISOString() : null,
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
export const setVirtualUserSchedule = pikkuFunc({
|
|
551
|
+
tags: ['pikku'],
|
|
552
|
+
title: 'Put a Virtual User on a Clock',
|
|
553
|
+
description:
|
|
554
|
+
'Says how often a persona should use the application on its own. Off until enabled, and every field left out keeps what it already had.',
|
|
555
|
+
expose: true,
|
|
556
|
+
// Its own scope, and the most powerful one here: starting a run spends money
|
|
557
|
+
// once with a caller present to see it, while writing a schedule spends it
|
|
558
|
+
// repeatedly with nobody there.
|
|
559
|
+
scopes: ['virtualUser:schedule'],
|
|
560
|
+
input: SetVirtualUserScheduleInput,
|
|
561
|
+
output: SetVirtualUserScheduleOutput,
|
|
562
|
+
func: async ({ virtualUserScheduleStore }, input) => {
|
|
563
|
+
if (!virtualUserScheduleStore) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
'No virtualUserScheduleStore is wired — a cadence has nowhere to live. ' +
|
|
566
|
+
'Wire KyselyVirtualUserScheduleStore from @pikku/kysely, or your own implementation of VirtualUserScheduleStore.'
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
const persona = personaConfigs[
|
|
570
|
+
input.persona as keyof typeof personaConfigs
|
|
571
|
+
] as { runnable: boolean } | undefined
|
|
572
|
+
if (!persona) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
\`Unknown persona "\${input.persona}" — declare it with definePersonas()\`
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
// The same rule \`runVirtualUser\` enforces, applied at the point the row is
|
|
578
|
+
// written rather than every hour afterwards: an acted-upon persona has no
|
|
579
|
+
// session, so a cadence for one is a tick that can only ever fail to start.
|
|
580
|
+
if (!persona.runnable) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
\`Persona "\${input.persona}" is declared as acted upon, never run\`
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
const schedule = await virtualUserScheduleStore.set({
|
|
586
|
+
persona: input.persona,
|
|
587
|
+
enabled: input.enabled,
|
|
588
|
+
disposition: input.disposition as VirtualUserDisposition | undefined,
|
|
589
|
+
goals: input.goals,
|
|
590
|
+
budget:
|
|
591
|
+
input.budget === undefined
|
|
592
|
+
? undefined
|
|
593
|
+
: input.budget === null
|
|
594
|
+
? null
|
|
595
|
+
: {
|
|
596
|
+
steps: input.budget.steps,
|
|
597
|
+
mutations: input.budget.mutations,
|
|
598
|
+
duration: input.budget.durationMs,
|
|
599
|
+
},
|
|
600
|
+
minIntervalMs: input.minIntervalMs,
|
|
601
|
+
maxIntervalMs: input.maxIntervalMs,
|
|
602
|
+
nextRunAt: input.nextRunAt ? new Date(input.nextRunAt) : undefined,
|
|
603
|
+
})
|
|
604
|
+
return serializeSchedule(schedule)
|
|
605
|
+
},
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
export const listVirtualUserSchedules = pikkuFunc({
|
|
609
|
+
tags: ['pikku'],
|
|
610
|
+
title: 'List Virtual User Schedules',
|
|
611
|
+
description:
|
|
612
|
+
'Which personas are on a clock, how often they run, and when each is next due.',
|
|
613
|
+
expose: true,
|
|
614
|
+
scopes: ['virtualUser:read'],
|
|
615
|
+
input: null,
|
|
616
|
+
output: ListVirtualUserSchedulesOutput,
|
|
617
|
+
func: async ({ virtualUserScheduleStore }) => {
|
|
618
|
+
if (!virtualUserScheduleStore) {
|
|
619
|
+
return { schedules: [] }
|
|
620
|
+
}
|
|
621
|
+
const schedules = await virtualUserScheduleStore.list()
|
|
622
|
+
return { schedules: schedules.map(serializeSchedule) }
|
|
623
|
+
},
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Acts on whichever personas are due, once.
|
|
628
|
+
*
|
|
629
|
+
* Not exposed, and not wired to anything: pikku does not start a timer on an
|
|
630
|
+
* application's behalf. A scaffolded scheduled task would begin spending model
|
|
631
|
+
* budget the moment a project ran \`pikku all\`, which is not a thing a codegen
|
|
632
|
+
* step gets to decide. Wire it when you mean it:
|
|
633
|
+
*
|
|
634
|
+
* \`\`\`ts
|
|
635
|
+
* wireScheduler({
|
|
636
|
+
* name: 'virtualUsers',
|
|
637
|
+
* schedule: '0 * * * *',
|
|
638
|
+
* func: tickVirtualUserSchedules,
|
|
639
|
+
* })
|
|
640
|
+
* \`\`\`
|
|
641
|
+
*
|
|
642
|
+
* An hourly tick is plenty for intervals measured in hours: the tick
|
|
643
|
+
* decides nothing except which rows are already due, so running it more often
|
|
644
|
+
* costs a query and changes no cadence.
|
|
645
|
+
*/
|
|
646
|
+
export const tickVirtualUserSchedules = pikkuSessionlessFunc<void, void>({
|
|
647
|
+
tags: ['pikku'],
|
|
648
|
+
func: async (services, _data, { rpc }) => {
|
|
649
|
+
const { virtualUserScheduleStore, virtualUserRunStore, logger } = services
|
|
650
|
+
if (!virtualUserScheduleStore || !virtualUserRunStore) {
|
|
651
|
+
return
|
|
652
|
+
}
|
|
653
|
+
const result = await tick({
|
|
654
|
+
schedules: virtualUserScheduleStore,
|
|
655
|
+
runs: virtualUserRunStore,
|
|
656
|
+
dispatch: (schedule) =>
|
|
657
|
+
startVirtualUserRun(services, rpc!, {
|
|
658
|
+
persona: schedule.persona,
|
|
659
|
+
disposition: schedule.disposition,
|
|
660
|
+
goals: schedule.goals,
|
|
661
|
+
budget: schedule.budget
|
|
662
|
+
? {
|
|
663
|
+
steps: schedule.budget.steps,
|
|
664
|
+
mutations: schedule.budget.mutations,
|
|
665
|
+
durationMs:
|
|
666
|
+
typeof schedule.budget.duration === 'number'
|
|
667
|
+
? schedule.budget.duration
|
|
668
|
+
: undefined,
|
|
669
|
+
}
|
|
670
|
+
: undefined,
|
|
671
|
+
}),
|
|
672
|
+
})
|
|
673
|
+
// Logged rather than returned: the caller is a cron, and a run this started
|
|
674
|
+
// is otherwise the only trace that a persona is still out there working.
|
|
675
|
+
for (const { persona, runId } of result.dispatched) {
|
|
676
|
+
logger.info(\`Virtual user \${persona} started run \${runId} on schedule\`)
|
|
677
|
+
}
|
|
678
|
+
for (const runId of result.reaped) {
|
|
679
|
+
logger.warn(
|
|
680
|
+
\`Virtual user run \${runId} was abandoned — marked failed so its persona can run again\`
|
|
681
|
+
)
|
|
682
|
+
}
|
|
683
|
+
for (const { persona, reason } of result.skipped) {
|
|
684
|
+
logger.info(\`Virtual user \${persona} skipped this tick: \${reason}\`)
|
|
304
685
|
}
|
|
305
686
|
},
|
|
306
687
|
})
|
|
@@ -415,6 +796,8 @@ export const executeVirtualUserRun = pikkuSessionlessFunc({
|
|
|
415
796
|
tally: result.tally,
|
|
416
797
|
memory: result.memory,
|
|
417
798
|
stoppedBy: result.stoppedBy ?? null,
|
|
799
|
+
intents: result.intents,
|
|
800
|
+
steps: result.steps,
|
|
418
801
|
})
|
|
419
802
|
|
|
420
803
|
return { findings: result.findings.length }
|