@nanobpm/urban 0.7.0 → 0.9.0

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.
@@ -5,6 +5,42 @@
5
5
  import type { AppApi, Mounted, RuntimeContext } from "../context.ts";
6
6
  import type { EngineJob, JobHandler, WorkerSubscription } from "../host.ts";
7
7
  import { workerJobType, type WorkerDecl } from "../manifest.ts";
8
+ import { type DecisionEvaluator, type LlmRuntime, runLlmJob } from "./llm.ts";
9
+
10
+ /** The subset of the engine SDK the LLM decision-rails need: evaluate a DMN decision,
11
+ * whose `output` comes back as a JSON string (the orchestration-cluster contract). */
12
+ interface DecisionCapableSdk {
13
+ evaluateDecision(input: {
14
+ decisionDefinitionId: string;
15
+ variables: Record<string, unknown>;
16
+ }): Promise<{ output?: unknown }>;
17
+ }
18
+
19
+ /** Adapt an engine SDK client into the LLM module's `DecisionEvaluator`, parsing the
20
+ * decision's JSON-string `output` into the value fed back to the LLM job. Validates the
21
+ * SDK shape and adds decision context to a parse failure rather than surfacing a raw
22
+ * "is not a function" / `JSON.parse` error. */
23
+ export function sdkDecisionEvaluator(sdk: unknown): DecisionEvaluator {
24
+ const client = sdk as Partial<DecisionCapableSdk>;
25
+ return async (decisionId, variables) => {
26
+ if (typeof client.evaluateDecision !== "function") {
27
+ throw new Error(
28
+ `llm worker: the engine SDK does not support evaluateDecision ` +
29
+ `(needed for decision rails "${decisionId}")`,
30
+ );
31
+ }
32
+ const res = await client.evaluateDecision({ decisionDefinitionId: decisionId, variables });
33
+ const out = res?.output;
34
+ if (typeof out !== "string") return out;
35
+ try {
36
+ return JSON.parse(out);
37
+ } catch {
38
+ throw new Error(
39
+ `llm worker: decision "${decisionId}" returned unparseable JSON output: ${out.slice(0, 300)}`,
40
+ );
41
+ }
42
+ };
43
+ }
8
44
 
9
45
  /** A handler as authored by an app: the job plus the injected app API. */
10
46
  export type AppJobHandler = (
@@ -59,12 +95,33 @@ export async function mountWorkers(ctx: RuntimeContext, app: AppApi): Promise<Wo
59
95
  const jobType = workerJobType(decl);
60
96
  if (!jobType) continue;
61
97
  if (!decl.handler) {
62
- // llm-backed workers (schema `oneOf` handler|llm) are validated but not yet
63
- // executed by the runtime. Skip with a clear warning rather than crashing.
64
- ctx.host.log("warn", "skipping worker: llm-backed workers are not yet implemented", {
65
- jobType,
66
- llm: decl.llm,
98
+ // LLM-backed worker (schema `oneOf` handler|llm): synthesise a handler that runs
99
+ // the job through the bound LLM (and optional decision rails) instead of loading a
100
+ // handler module. A declared-but-unknown binding is a manifest error → fail loudly.
101
+ if (!decl.llm) {
102
+ ctx.host.log("warn", "skipping worker: neither a handler nor an llm binding declared", {
103
+ jobType,
104
+ });
105
+ continue;
106
+ }
107
+ const binding = (ctx.manifest.llm ?? {})[decl.llm];
108
+ if (!binding) {
109
+ throw new Error(
110
+ `worker "${jobType}" references unknown llm binding "${decl.llm}" ` +
111
+ `(no llm["${decl.llm}"] in the manifest)`,
112
+ );
113
+ }
114
+ const rt: LlmRuntime = {
115
+ env: (n) => app.env(n),
116
+ evaluateDecision: app.sdk ? sdkDecisionEvaluator(app.sdk) : undefined,
117
+ };
118
+ const wrapped: JobHandler = (job) => runLlmJob(job.variables, binding, rt);
119
+ const sub = await ctx.engine.registerWorker(jobType, wrapped, {
120
+ workerName: `${ctx.manifest.id}:${jobType}`,
67
121
  });
122
+ subs.push(sub);
123
+ jobTypes.push(jobType);
124
+ ctx.host.log("info", "llm worker registered", { jobType, llm: decl.llm });
68
125
  continue;
69
126
  }
70
127
  const mod = await loadModule(decl.handler);
@@ -29,6 +29,14 @@ test("unknown top-level keys are rejected (additionalProperties:false)", () => {
29
29
  assert.ok(issues.some((i) => i.path === "bogus"));
30
30
  });
31
31
 
32
+ test("an `actions` array is an accepted top-level key (ADR 0055 §3)", () => {
33
+ const issues = collectManifestIssues({
34
+ ...valid,
35
+ actions: [{ path: "/app/actions/cancel", module: "actions/cancel.ts" }],
36
+ });
37
+ assert.deepEqual(issues, []);
38
+ });
39
+
32
40
  test("bad schemaVersion and bad slug id are reported", () => {
33
41
  const issues = collectManifestIssues({ ...valid, schemaVersion: 2, id: "Not A Slug" });
34
42
  assert.ok(issues.some((i) => i.path === "schemaVersion"));
@@ -17,9 +17,11 @@ export {
17
17
  workerJobType,
18
18
  } from "./core/manifest.ts";
19
19
  export type {
20
+ ActionDecl,
20
21
  AppManifest,
21
22
  DataSource,
22
23
  DomainType,
24
+ LlmBinding,
23
25
  SurfaceDecl,
24
26
  TriggerDecl,
25
27
  TypeField,
@@ -63,8 +65,24 @@ export type {
63
65
  } from "./core/modules/gateway.ts";
64
66
  export { createPagesRoutes, mountPages } from "./core/modules/pages.ts";
65
67
  export type { PagesDataSource, PagesDeps, PagesHandle, PagesOptions } from "./core/modules/pages.ts";
68
+ export { mountActions, resolveActionHandler } from "./core/modules/actions.ts";
69
+ export type { ActionHandler, ActionRequest, ActionResult, ActionsHandle } from "./core/modules/actions.ts";
66
70
  export type { AppJobHandler } from "./core/modules/workers.ts";
67
- export { resolveHandler } from "./core/modules/workers.ts";
71
+ export { resolveHandler, sdkDecisionEvaluator } from "./core/modules/workers.ts";
72
+ export {
73
+ buildMessages,
74
+ callLlm,
75
+ resolveProvider,
76
+ runLlmJob,
77
+ } from "./core/modules/llm.ts";
78
+ export type {
79
+ ChatMessage,
80
+ DecisionEvaluator,
81
+ EnvLookup,
82
+ LlmRuntime,
83
+ LlmVars,
84
+ ProviderConfig,
85
+ } from "./core/modules/llm.ts";
68
86
  export { evalCorrelation } from "./core/modules/triggers.ts";
69
87
 
70
88
  // Adapters + engine + run entrypoint
@@ -52,6 +52,11 @@
52
52
  "additionalProperties": { "$ref": "#/$defs/connection" }
53
53
  },
54
54
  "surfaces": { "$ref": "#/$defs/surfaces" },
55
+ "actions": {
56
+ "type": "array",
57
+ "items": { "$ref": "#/$defs/action" },
58
+ "description": "App-authored action handler overrides (ADR 0055 §3): each binds an HTTP route to a handler module that wraps the generic pages start/cancel/message actions with business logic, injected with the app's runtime API."
59
+ },
55
60
  "workers": {
56
61
  "type": "array",
57
62
  "items": { "$ref": "#/$defs/worker" },
@@ -367,6 +372,33 @@
367
372
  }
368
373
  }
369
374
  },
375
+ "action": {
376
+ "type": "object",
377
+ "additionalProperties": false,
378
+ "required": ["path", "module"],
379
+ "description": "An app-authored action handler override (ADR 0055 §3): binds a route to a handler module (default-exports an ActionHandler). Mounted before the generic pages action routes, so an exact override shadows the generic one.",
380
+ "properties": {
381
+ "path": {
382
+ "type": "string",
383
+ "minLength": 1,
384
+ "description": "Route path to serve, e.g. '/app/actions/cancel' or '/app/actions/start/convergence-loop'."
385
+ },
386
+ "module": {
387
+ "type": "string",
388
+ "minLength": 1,
389
+ "description": "Handler module path relative to the app root; default-exports (or names `handler`) an ActionHandler."
390
+ },
391
+ "method": {
392
+ "type": "string",
393
+ "minLength": 1,
394
+ "description": "HTTP method to match. Default 'POST'."
395
+ },
396
+ "prefix": {
397
+ "type": "boolean",
398
+ "description": "Match `path` as a prefix rather than exactly. Default false."
399
+ }
400
+ }
401
+ },
370
402
  "llmBinding": {
371
403
  "type": "object",
372
404
  "additionalProperties": false,