-
Write an ordinary tinystruct action and declare every parameter in
@Action(arguments = ...), in order:@Action(value = "close-ticket", description = "Close and resolve an existing ticket, by its number.", arguments = {@Argument(key = "ticketId", type = "number", description = "The ticket number.")}) public String closeTicket(int ticketId) { ... }
-
Add it to
typesafe.routing.allowed-actions. If it is destructive, also totypesafe.routing.confirm-actions, and consider a strictertypesafe.routing.min-confidence.<action>. -
Try it from the terminal, first without the model:
bin/dispatcher close-ticket/42 --import my.HelpDesk
then through the dispatcher.
Guidelines for what the model reads:
- Descriptions are literal instructions. State boundaries ("Close a ticket. Does not delete it.").
- Jev is weak at arithmetic, counting and dates. Take numbers as text and parse them in the action; do not ask the model to compute.
- Keep enum constants self-explanatory; list them in the parameter description.
DispatchPipeline takes everything through its constructor, so a test needs no application server and no network:
MockTypesafeClient client = new MockTypesafeClient();
client.addChoiceAnswer("__tool__", "create-user", 0.95);
client.addChoiceAnswer("create-user.name", "John", 0.97);
client.addChoiceAnswer("create-user.role", "ADMIN", 0.96);
DispatchPipeline pipeline = DispatchPipeline.of(client, new TokenSpanExtractor(),
new ConfidencePolicy(0.80, Double.NaN, 0.5), new ArgumentValidator(),
new PathActionExecutor(), /* confirmation */ null, new DefaultPrincipalResolver(),
settings, new DispatchMetrics());
DispatchResult result = pipeline.route("create an admin account for John", ActionRegistry.getInstance());Install your application into the real registry first, exactly as --import would:
ApplicationManager.install(new MyApp(), new Settings()).
Answer keys: __tool__, <action>.<param>, <action>.<param>? (optional: was it stated), <action>.<param>.<MEMBER> (set member). Reserved options: __other__ (no action applies) and __none__ (not stated).
tinystruct's Settings is one process-wide property store, and set rewrites application.properties when one is on the classpath. Tests that set values through it leak into each other and can modify files. Use an isolated Configuration<String> (see MapConfiguration in the core tests).
public class MyService implements ConfirmationService {
public MyService() {} // created by class name
@Override public void configure(Configuration<String> config) { ... }
@Override public String open(PendingCall call) { ... } // store; return an id
@Override public DispatchResult confirm(String id, String principal) { ... }
@Override public void reject(String id, String principal) { ... }
}Set typesafe.confirmation.service=my.MyService. Contract:
confirmandrejectmust verifyprincipalequalscall.getPrincipal()(throwPrincipalMismatchException) and that the call has not expired (ConfirmationExpiredException).- A second
confirmmust not run the action again (ConfirmationConflictException). - To run the call, use
ConfirmedCallRunner(TypesafeRuntime.shared(config).confirmedCalls()): it re-checks the allowlist, mode and arguments. - Do not keep the arguments longer than needed; they can be personal data.
PendingCall.toString()deliberately omits them.
public String resolve(Context context) // stable identity of the caller, never derived from request parametersThe default reads a validated JWT (CLAIMS), then the session userId, then the session id, else cli. Anything you return must be something the caller cannot choose.
- Pin the model (
typesafe.model=jev-1.13.0). The cache key includes the model name, so a pinned model keeps entries valid and an alias change misses cleanly. - Cache:
memoryfor one process,redisfor several. Only classifications are cached, never results of executed actions. A change to the allowlist or to any description changes the request, so it misses on its own. - Snapshots: use
file,redisordatabaseoutside tests, restrict access, and encrypt at rest. Untouched pending calls stay until removed; a periodic clean-up of old snapshots is worth adding. - Rate limits: 429 and 529 are retried with backoff (
typesafe.retry-max,typesafe.retry-backoff-ms).typesafe/metricsreports upstream calls, failures, cache hits, tokens, routing outcomes, confidence buckets, per-action counts and confirmation outcomes. - Logs never contain argument values unless
typesafe.logging.log-arguments=true, and never the API key. - Cost: one request carries every question. With many allowlisted actions use
typesafe.routing.strategy=two-stage; a choice question holds at most 255 options.
mvn verifyclient, core and workflow must stay at 90% line coverage. RedisRoutingCache needs a live Redis and is not exercised by the unit tests.