veryfront 0.1.614 → 0.1.616

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.
Files changed (65) hide show
  1. package/esm/cli/templates/manifest.d.ts +1 -0
  2. package/esm/cli/templates/manifest.js +2 -1
  3. package/esm/deno.d.ts +3 -0
  4. package/esm/deno.js +4 -1
  5. package/esm/src/agent/hosted/child-stream-watchdog.d.ts.map +1 -1
  6. package/esm/src/agent/hosted/child-stream-watchdog.js +47 -29
  7. package/esm/src/agent/hosted/cloud-prepared-chat-execution-runtime.d.ts +9 -0
  8. package/esm/src/agent/hosted/cloud-prepared-chat-execution-runtime.d.ts.map +1 -1
  9. package/esm/src/agent/hosted/cloud-prepared-chat-execution-runtime.js +26 -1
  10. package/esm/src/agent/hosted/stream-terminal-error.d.ts.map +1 -1
  11. package/esm/src/agent/hosted/stream-terminal-error.js +44 -0
  12. package/esm/src/agent/identity-contracts.d.ts +42 -0
  13. package/esm/src/agent/identity-contracts.d.ts.map +1 -0
  14. package/esm/src/agent/identity-contracts.js +25 -0
  15. package/esm/src/agent/index.d.ts +1 -0
  16. package/esm/src/agent/index.d.ts.map +1 -1
  17. package/esm/src/agent/index.js +1 -0
  18. package/esm/src/chat/message-prep.d.ts.map +1 -1
  19. package/esm/src/chat/message-prep.js +16 -2
  20. package/esm/src/integrations/_data.js +1 -1
  21. package/esm/src/integrations/_tool_summaries.d.ts.map +1 -1
  22. package/esm/src/integrations/_tool_summaries.js +197 -4
  23. package/esm/src/integrations/schema.d.ts +8 -0
  24. package/esm/src/integrations/schema.d.ts.map +1 -1
  25. package/esm/src/integrations/schema.js +2 -1
  26. package/esm/src/oauth/providers/base.d.ts +9 -0
  27. package/esm/src/oauth/providers/base.d.ts.map +1 -1
  28. package/esm/src/oauth/providers/base.js +23 -3
  29. package/esm/src/platform/adapters/redis/deno.d.ts +1 -0
  30. package/esm/src/platform/adapters/redis/deno.d.ts.map +1 -1
  31. package/esm/src/platform/adapters/redis/deno.js +5 -0
  32. package/esm/src/platform/adapters/redis/interface.d.ts +5 -0
  33. package/esm/src/platform/adapters/redis/interface.d.ts.map +1 -1
  34. package/esm/src/platform/adapters/redis/node.d.ts +1 -0
  35. package/esm/src/platform/adapters/redis/node.d.ts.map +1 -1
  36. package/esm/src/platform/adapters/redis/node.js +5 -0
  37. package/esm/src/platform/adapters/redis/types.d.ts +5 -0
  38. package/esm/src/platform/adapters/redis/types.d.ts.map +1 -1
  39. package/esm/src/provider/local/local-engine.d.ts +32 -0
  40. package/esm/src/provider/local/local-engine.d.ts.map +1 -1
  41. package/esm/src/provider/local/local-engine.js +66 -9
  42. package/esm/src/provider/runtime-loader/provider-usage.d.ts.map +1 -1
  43. package/esm/src/provider/runtime-loader/provider-usage.js +12 -1
  44. package/esm/src/rendering/rsc/server-renderer/tree-processor.js +5 -5
  45. package/esm/src/routing/api/api-route-matcher.d.ts.map +1 -1
  46. package/esm/src/routing/api/api-route-matcher.js +3 -2
  47. package/esm/src/routing/matchers/decode-param.d.ts +9 -0
  48. package/esm/src/routing/matchers/decode-param.d.ts.map +1 -0
  49. package/esm/src/routing/matchers/decode-param.js +17 -0
  50. package/esm/src/routing/matchers/route-matcher.d.ts.map +1 -1
  51. package/esm/src/routing/matchers/route-matcher.js +3 -4
  52. package/esm/src/routing/slug-mapper/dynamic-route-matcher.d.ts.map +1 -1
  53. package/esm/src/routing/slug-mapper/dynamic-route-matcher.js +8 -2
  54. package/esm/src/server/node-handler.d.ts.map +1 -1
  55. package/esm/src/server/node-handler.js +40 -5
  56. package/esm/src/tool/factory.d.ts.map +1 -1
  57. package/esm/src/tool/factory.js +4 -3
  58. package/esm/src/utils/version-constant.d.ts +1 -1
  59. package/esm/src/utils/version-constant.js +1 -1
  60. package/esm/src/workflow/backends/redis/index.d.ts +2 -0
  61. package/esm/src/workflow/backends/redis/index.d.ts.map +1 -1
  62. package/esm/src/workflow/backends/redis/index.js +39 -6
  63. package/esm/src/workflow/executor/dag/index.d.ts.map +1 -1
  64. package/esm/src/workflow/executor/dag/index.js +30 -5
  65. package/package.json +5 -1
@@ -18,6 +18,70 @@ import { throwIfLocalAIDisabled } from "./env.js";
18
18
  const logger = serverLogger.component("local-llm");
19
19
  /** Default maximum new tokens for local model generation */
20
20
  const DEFAULT_MAX_NEW_TOKENS = 512;
21
+ /**
22
+ * Build a StoppingCriteriaList that halts generation as soon as any of the
23
+ * provided stop strings appears in the *newly generated* output.
24
+ *
25
+ * Transformers.js (>=3.x) does not expose a `stop_strings` generate option,
26
+ * so we decode the running sequence with the tokenizer and match the stop
27
+ * strings against the suffix — the documented `stopping_criteria` mechanism.
28
+ *
29
+ * Transformers.js passes the full sequence (prompt + generated tokens) to
30
+ * `_call` on every step. We must scan only the generated suffix: if a system
31
+ * or user message contains a configured stop string (e.g. an instruction that
32
+ * mentions "END"), scanning the whole sequence would return `true` on the very
33
+ * first generation step and truncate the response to empty.
34
+ *
35
+ * The prompt token length is not cheaply known where this list is built (the
36
+ * pipeline tokenizes `messages` internally), so per batch item we self-calibrate
37
+ * on the first `_call`: the sequence length seen on the first invocation is
38
+ * recorded as the prompt boundary, and only tokens at or after that boundary are
39
+ * decoded and matched on subsequent steps.
40
+ */
41
+ function buildStopStringCriteria(transformers, tokenizer, stopSequences) {
42
+ const list = new transformers.StoppingCriteriaList();
43
+ const base = new transformers.StoppingCriteria();
44
+ const criterion = base;
45
+ // Per-batch-item prompt token length, captured on the first invocation.
46
+ const promptLengths = [];
47
+ criterion._call = (inputIds) => inputIds.map((ids, item) => {
48
+ if (promptLengths[item] === undefined) {
49
+ // First step for this item: everything seen so far is prompt. Record the
50
+ // boundary and never trip on the prompt itself.
51
+ promptLengths[item] = ids.length;
52
+ return false;
53
+ }
54
+ const generated = ids.slice(promptLengths[item]);
55
+ if (generated.length === 0)
56
+ return false;
57
+ const text = tokenizer.decode(generated);
58
+ return stopSequences.some((stop) => stop.length > 0 && text.includes(stop));
59
+ });
60
+ list.push(criterion);
61
+ return list;
62
+ }
63
+ /**
64
+ * Translate engine-level {@link GenerateOptions} into the options object passed
65
+ * to the Transformers.js text-generation pipeline.
66
+ *
67
+ * Exported for unit testing the option-forwarding seam (notably that
68
+ * `stopSequences` is not silently dropped) without downloading a model.
69
+ */
70
+ export function buildPipeOptions(options, transformers, tokenizer, streamer) {
71
+ const { maxNewTokens = DEFAULT_MAX_NEW_TOKENS, temperature = 0.7, topP, topK, stopSequences, } = options;
72
+ const pipeOptions = {
73
+ max_new_tokens: maxNewTokens,
74
+ temperature,
75
+ top_p: topP,
76
+ top_k: topK,
77
+ do_sample: temperature > 0,
78
+ streamer,
79
+ };
80
+ if (stopSequences && stopSequences.length > 0) {
81
+ pipeOptions.stopping_criteria = buildStopStringCriteria(transformers, tokenizer, stopSequences);
82
+ }
83
+ return pipeOptions;
84
+ }
21
85
  /** Cached pipeline instances keyed by HuggingFace model ID */
22
86
  const pipelineCache = new Map();
23
87
  /** Whether a model is currently being loaded (prevents concurrent loads) */
@@ -132,7 +196,6 @@ export async function* generateStream(modelId, messages, options = {}) {
132
196
  const modelInfo = resolveLocalModel(modelId);
133
197
  const pipe = await loadPipeline(modelInfo);
134
198
  const transformers = await getTransformers();
135
- const { maxNewTokens = DEFAULT_MAX_NEW_TOKENS, temperature = 0.7, topP, topK, } = options;
136
199
  // Use a queue to bridge TextStreamer callbacks → async generator
137
200
  const tokenQueue = [];
138
201
  let resolveWaiting = null;
@@ -151,17 +214,11 @@ export async function* generateStream(modelId, messages, options = {}) {
151
214
  flushWaiting();
152
215
  },
153
216
  });
217
+ const pipeOptions = buildPipeOptions(options, transformers, pipe.tokenizer, streamer);
154
218
  // Start generation in the background
155
219
  const generatePromise = (async () => {
156
220
  try {
157
- await pipe(messages, {
158
- max_new_tokens: maxNewTokens,
159
- temperature,
160
- top_p: topP,
161
- top_k: topK,
162
- do_sample: temperature > 0,
163
- streamer,
164
- });
221
+ await pipe(messages, pipeOptions);
165
222
  }
166
223
  finally {
167
224
  done = true;
@@ -1 +1 @@
1
- {"version":3,"file":"provider-usage.d.ts","sourceRoot":"","sources":["../../../../src/src/provider/runtime-loader/provider-usage.ts"],"names":[],"mappings":"AAEA,6CAA6C;AAC7C,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAsBhF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAoB7E;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAmB7E;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAwBtF;AAED,qCAAqC;AACrC,wBAAgB,UAAU,CACxB,OAAO,EAAE,YAAY,GAAG,SAAS,EACjC,IAAI,EAAE,YAAY,GAAG,SAAS,GAC7B,YAAY,GAAG,SAAS,CAsB1B"}
1
+ {"version":3,"file":"provider-usage.d.ts","sourceRoot":"","sources":["../../../../src/src/provider/runtime-loader/provider-usage.ts"],"names":[],"mappings":"AAEA,6CAA6C;AAC7C,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAsBhF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAoB7E;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAmB7E;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAwBtF;AAED,qCAAqC;AACrC,wBAAgB,UAAU,CACxB,OAAO,EAAE,YAAY,GAAG,SAAS,EACjC,IAAI,EAAE,YAAY,GAAG,SAAS,GAC7B,YAAY,GAAG,SAAS,CAkC1B"}
@@ -100,10 +100,21 @@ export function mergeUsage(current, next) {
100
100
  const cacheCreationInputTokens = next.cacheCreationInputTokens ??
101
101
  current.cacheCreationInputTokens;
102
102
  const cacheReadInputTokens = next.cacheReadInputTokens ?? current.cacheReadInputTokens;
103
+ // Prefer the provider-reported total (latest non-undefined wins, matching the
104
+ // ?? semantics used for input/output above). Providers like Gemini 2.5
105
+ // thinking models and OpenAI reasoning models report a total that exceeds
106
+ // input + output because it includes reasoning/thoughts tokens. Recomputing
107
+ // the sum would discard those, undercounting usage. Take the larger of the
108
+ // provider total and the recomputed sum so we never undercount.
109
+ const reportedTotal = next.totalTokens ?? current.totalTokens;
110
+ const recomputedTotal = (inputTokens ?? 0) + (outputTokens ?? 0);
111
+ const totalTokens = reportedTotal !== undefined
112
+ ? Math.max(reportedTotal, recomputedTotal)
113
+ : recomputedTotal;
103
114
  return {
104
115
  inputTokens,
105
116
  outputTokens,
106
- totalTokens: (inputTokens ?? 0) + (outputTokens ?? 0),
117
+ totalTokens,
107
118
  ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
108
119
  ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
109
120
  };
@@ -2,7 +2,7 @@ import * as React from "../../../../react/react.js";
2
2
  import { serverLogger } from "../../../utils/index.js";
3
3
  import { renderToStringAdapter } from "../../../react/index.js";
4
4
  import { getComponentId, isClientComponent, registerClientRef, } from "./component-detector.js";
5
- import { renderAttributes, treeToHTML } from "./html-generator.js";
5
+ import { escapeHtml, renderAttributes, treeToHTML } from "./html-generator.js";
6
6
  import { serializeProps } from "./prop-serializer.js";
7
7
  const logger = serverLogger.component("rsc");
8
8
  /** Recursively renders a component tree to RSC nodes */
@@ -10,13 +10,13 @@ export async function renderTree(
10
10
  // deno-lint-ignore no-explicit-any -- RSC components accept arbitrary props at runtime
11
11
  Component, props, clientManifest, clientRefs) {
12
12
  if (Component == null || typeof Component === "string" || typeof Component === "number") {
13
- return { type: "html", html: Component == null ? "" : String(Component) };
13
+ return { type: "html", html: Component == null ? "" : escapeHtml(String(Component)) };
14
14
  }
15
15
  if (React.isValidElement(Component)) {
16
16
  return processElement(Component, clientManifest, clientRefs);
17
17
  }
18
18
  if (typeof Component !== "function") {
19
- return { type: "html", html: String(Component) };
19
+ return { type: "html", html: escapeHtml(String(Component)) };
20
20
  }
21
21
  const rscComponent = Component;
22
22
  if (isClientComponent(rscComponent, clientManifest)) {
@@ -36,7 +36,7 @@ Component, props, clientManifest, clientRefs) {
36
36
  return { type: "html", html: "" };
37
37
  if (React.isValidElement(element))
38
38
  return processElement(element, clientManifest, clientRefs);
39
- return { type: "html", html: String(element) };
39
+ return { type: "html", html: escapeHtml(String(element)) };
40
40
  }
41
41
  catch (error) {
42
42
  logger.error("Error rendering component:", error);
@@ -76,6 +76,6 @@ export function renderChildren(children, clientManifest, clientRefs) {
76
76
  if (React.isValidElement(child)) {
77
77
  return processElement(child, clientManifest, clientRefs);
78
78
  }
79
- return Promise.resolve({ type: "html", html: String(child) });
79
+ return Promise.resolve({ type: "html", html: escapeHtml(String(child)) });
80
80
  }));
81
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"api-route-matcher.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/api/api-route-matcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAU9D,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAElC,mDAAmD;AACnD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,UAAU,CAAsC;;IAUxD,wCAAwC;IACxC,IAAI,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAEpC;IAED,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAkD7C,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,oBAAoB;IAgB5B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAoBtC,OAAO,CAAC,aAAa;IAiCrB,UAAU,IAAI,KAAK,EAAE;IAIrB,KAAK,IAAI,IAAI;IAKb,UAAU,IAAI,IAAI;IAIlB,OAAO,IAAI,IAAI;CAGhB"}
1
+ {"version":3,"file":"api-route-matcher.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/api/api-route-matcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAW9D,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAElC,mDAAmD;AACnD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,UAAU,CAAsC;;IAUxD,wCAAwC;IACxC,IAAI,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAEpC;IAED,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAkD7C,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,oBAAoB;IAgB5B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAoBtC,OAAO,CAAC,aAAa;IAiCrB,UAAU,IAAI,KAAK,EAAE;IAIrB,KAAK,IAAI,IAAI;IAKb,UAAU,IAAI,IAAI;IAIlB,OAAO,IAAI,IAAI;CAGhB"}
@@ -1,6 +1,7 @@
1
1
  import * as dntShim from "../../../_dnt.shims.js";
2
2
  import { getDisableLruIntervalEnv } from "../../config/env.js";
3
3
  import { LRUCache } from "../../utils/lru-wrapper.js";
4
+ import { safeDecodeParam } from "../matchers/decode-param.js";
4
5
  /** Max entries in the route-match LRU cache */
5
6
  const ROUTE_CACHE_MAX_ENTRIES = 500;
6
7
  /** Time-to-live for cached route matches (5 minutes) */
@@ -121,10 +122,10 @@ export class ApiRouteMatcher {
121
122
  const value = match[i + 1];
122
123
  if (catchAllParamNames.has(paramName)) {
123
124
  const segments = value ? value.split("/").filter((segment) => segment.length > 0) : [];
124
- params[paramName] = segments.map((segment) => decodeURIComponent(segment));
125
+ params[paramName] = segments.map((segment) => safeDecodeParam(segment));
125
126
  continue;
126
127
  }
127
- params[paramName] = decodeURIComponent(value ?? "");
128
+ params[paramName] = safeDecodeParam(value ?? "");
128
129
  }
129
130
  return params;
130
131
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Decode a single URL path segment, tolerating malformed percent-encoding.
3
+ *
4
+ * `decodeURIComponent` throws `URIError` on invalid sequences (e.g. `%zz`).
5
+ * For route param extraction we prefer to fall back to the raw value so a
6
+ * malformed request yields a normal route miss / 4xx instead of crashing.
7
+ */
8
+ export declare function safeDecodeParam(value: string): string;
9
+ //# sourceMappingURL=decode-param.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-param.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/matchers/decode-param.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOrD"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Decode a single URL path segment, tolerating malformed percent-encoding.
3
+ *
4
+ * `decodeURIComponent` throws `URIError` on invalid sequences (e.g. `%zz`).
5
+ * For route param extraction we prefer to fall back to the raw value so a
6
+ * malformed request yields a normal route miss / 4xx instead of crashing.
7
+ */
8
+ export function safeDecodeParam(value) {
9
+ try {
10
+ return decodeURIComponent(value);
11
+ }
12
+ catch (error) {
13
+ if (error instanceof URIError)
14
+ return value;
15
+ throw error;
16
+ }
17
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/matchers/route-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAyBpD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,UAAU,GAAG,IAAI,CAe5E"}
1
+ {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/matchers/route-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA0BpD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,UAAU,GAAG,IAAI,CAa5E"}
@@ -1,3 +1,4 @@
1
+ import { safeDecodeParam } from "./decode-param.js";
1
2
  const CATCH_ALL_PATTERN = /\[\[?\.\.\.(\w+)\]\]?/g;
2
3
  function extractCatchAllParams(pattern) {
3
4
  const params = new Set();
@@ -16,7 +17,7 @@ function decodeCatchAllValue(value) {
16
17
  return value
17
18
  .split("/")
18
19
  .filter(Boolean)
19
- .map((segment) => decodeURIComponent(segment));
20
+ .map((segment) => safeDecodeParam(segment));
20
21
  }
21
22
  export function matchRoute(pathname, route) {
22
23
  const match = pathname.match(route.regex);
@@ -26,9 +27,7 @@ export function matchRoute(pathname, route) {
26
27
  const params = {};
27
28
  for (const [index, name] of (route.paramNames ?? []).entries()) {
28
29
  const value = match[index + 1] ?? "";
29
- params[name] = catchAllParams.has(name)
30
- ? decodeCatchAllValue(value)
31
- : decodeURIComponent(value);
30
+ params[name] = catchAllParams.has(name) ? decodeCatchAllValue(value) : safeDecodeParam(value);
32
31
  }
33
32
  return { params, route };
34
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-route-matcher.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/slug-mapper/dynamic-route-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAUjE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAsC/E;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAErE"}
1
+ {"version":3,"file":"dynamic-route-matcher.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/slug-mapper/dynamic-route-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAkBjE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAsC/E;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAErE"}
@@ -1,10 +1,16 @@
1
1
  export { isDynamicRoute } from "../../utils/route-path-utils.js";
2
+ function isOptionalSpreadParam(part) {
3
+ return part.startsWith("[[...") && part.endsWith("]]");
4
+ }
2
5
  function isSpreadParam(part) {
3
- return part.startsWith("[...") && part.endsWith("]");
6
+ return isOptionalSpreadParam(part) || (part.startsWith("[...") && part.endsWith("]"));
4
7
  }
5
8
  function isDynamicParam(part) {
6
9
  return part.startsWith("[") && part.endsWith("]");
7
10
  }
11
+ function spreadParamName(part) {
12
+ return isOptionalSpreadParam(part) ? part.slice(5, -2) : part.slice(4, -1);
13
+ }
8
14
  export function extractParams(pattern, slug) {
9
15
  const patternParts = pattern.split("/").filter(Boolean);
10
16
  const slugParts = slug.split("/").filter(Boolean);
@@ -16,7 +22,7 @@ export function extractParams(pattern, slug) {
16
22
  let slugIndex = 0;
17
23
  for (const patternPart of patternParts) {
18
24
  if (isSpreadParam(patternPart)) {
19
- const paramName = patternPart.slice(4, -1);
25
+ const paramName = spreadParamName(patternPart);
20
26
  params[paramName] = slugParts.slice(slugIndex);
21
27
  return params;
22
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"node-handler.d.ts","sourceRoot":"","sources":["../../../src/src/server/node-handler.ts"],"names":[],"mappings":"AAEA,sEAAsE;AACtE,wBAAgB,aAAa,CAC3B,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,GACtD,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,GAAG,EAAE,OAAO,WAAW,EAAE,cAAc,KAAK,IAAI,CAqC7F"}
1
+ {"version":3,"file":"node-handler.d.ts","sourceRoot":"","sources":["../../../src/src/server/node-handler.ts"],"names":[],"mappings":"AAEA,sEAAsE;AACtE,wBAAgB,aAAa,CAC3B,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,GACtD,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,GAAG,EAAE,OAAO,WAAW,EAAE,cAAc,KAAK,IAAI,CAqE7F"}
@@ -4,12 +4,14 @@ export function toNodeHandler(handler) {
4
4
  return async (req, res) => {
5
5
  try {
6
6
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
7
- const headers = {};
7
+ const headers = new Headers();
8
8
  for (const [key, value] of Object.entries(req.headers)) {
9
9
  if (typeof value === "string")
10
- headers[key] = value;
11
- else if (Array.isArray(value))
12
- headers[key] = value[0] ?? "";
10
+ headers.append(key, value);
11
+ else if (Array.isArray(value)) {
12
+ for (const entry of value)
13
+ headers.append(key, entry);
14
+ }
13
15
  }
14
16
  const method = req.method ?? "GET";
15
17
  const body = method === "GET" || method === "HEAD" ? null : req;
@@ -23,7 +25,40 @@ export function toNodeHandler(handler) {
23
25
  const response = await handler(new Request(url.toString(), init));
24
26
  if (response.status === 101)
25
27
  return;
26
- res.writeHead(response.status, Object.fromEntries(response.headers));
28
+ const outHeaders = {};
29
+ const setCookies = [];
30
+ // Headers.prototype.getSetCookie landed in Node ~18.14, but our published
31
+ // engines.node is ">=18.0.0". On early 18.x it is undefined, so calling it
32
+ // unconditionally throws for every response and turns valid requests into
33
+ // 500s. Feature-detect it and fall back to the header iterator.
34
+ const getSetCookie = response.headers.getSetCookie;
35
+ if (typeof getSetCookie === "function") {
36
+ // Modern path: getSetCookie returns each Set-Cookie as a distinct value.
37
+ setCookies.push(...getSetCookie.call(response.headers));
38
+ for (const [key, value] of response.headers) {
39
+ if (key.toLowerCase() === "set-cookie")
40
+ continue;
41
+ outHeaders[key] = value;
42
+ }
43
+ }
44
+ else {
45
+ // Fallback for runtimes without getSetCookie. The undici-based Headers
46
+ // iterator yields each Set-Cookie as its own entry (it is the one header
47
+ // that is NOT comma-joined during iteration), so iterating preserves
48
+ // multiples where the platform allows it. If a runtime does collapse
49
+ // them into a single comma-joined string we still pass that one value
50
+ // through unchanged rather than throwing — degrade gracefully, never 500.
51
+ for (const [key, value] of response.headers) {
52
+ if (key.toLowerCase() === "set-cookie") {
53
+ setCookies.push(value);
54
+ continue;
55
+ }
56
+ outHeaders[key] = value;
57
+ }
58
+ }
59
+ if (setCookies.length > 0)
60
+ outHeaders["Set-Cookie"] = setCookies;
61
+ res.writeHead(response.status, outHeaders);
27
62
  if (response.body) {
28
63
  const reader = response.body.getReader();
29
64
  while (true) {
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/src/tool/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,+BAA+B,CAAC;AAyJxE,sCAAsC;AACtC,wBAAgB,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,EACtD,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CA+CvB;AAED,0CAA0C;AAC1C,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,UAAU,CAAC;IAC7B,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,oBAAoB,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACxF,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAC7C,GAAG,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;CACzB;AAED,wCAAwC;AACxC,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CA4B7E"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/src/tool/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,+BAA+B,CAAC;AAyJxE,sCAAsC;AACtC,wBAAgB,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,EACtD,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAgDvB;AAED,0CAA0C;AAC1C,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,UAAU,CAAC;IAC7B,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,oBAAoB,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACxF,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAC7C,GAAG,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;CACzB;AAED,wCAAwC;AACxC,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CA4B7E"}
@@ -102,9 +102,10 @@ export function tool(config) {
102
102
  outputSchema: config.outputSchema,
103
103
  outputSchemaJson,
104
104
  execute: async (input, context) => {
105
+ let validated = input;
105
106
  if (hasSchemaParse(config.inputSchema)) {
106
107
  try {
107
- config.inputSchema.parse(input);
108
+ validated = config.inputSchema.parse(input);
108
109
  }
109
110
  catch (error) {
110
111
  throw toError(createError({
@@ -113,7 +114,7 @@ export function tool(config) {
113
114
  }));
114
115
  }
115
116
  }
116
- return await config.execute(input, context);
117
+ return await config.execute(validated, context);
117
118
  },
118
119
  mcp: config.mcp,
119
120
  };
@@ -133,7 +134,7 @@ export function dynamicTool(config) {
133
134
  inputSchemaJson,
134
135
  execute: async (input, context) => {
135
136
  if (hasSchemaParse(config.inputSchema)) {
136
- config.inputSchema.parse(input);
137
+ input = config.inputSchema.parse(input);
137
138
  }
138
139
  else if (input === undefined) {
139
140
  input = {};
@@ -1,3 +1,3 @@
1
1
  /** Shared version value. */
2
- export declare const VERSION = "0.1.614";
2
+ export declare const VERSION = "0.1.616";
3
3
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,4 +1,4 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
3
  /** Shared version value. */
4
- export const VERSION = "0.1.614";
4
+ export const VERSION = "0.1.616";
@@ -9,6 +9,8 @@ export declare class RedisBackend implements WorkflowBackend {
9
9
  private connectionPromise;
10
10
  private config;
11
11
  private initialized;
12
+ /** Per-run lock tokens for ownership-checked release/extend (Redlock pattern). */
13
+ private lockValues;
12
14
  constructor(config?: RedisBackendConfig);
13
15
  private runKey;
14
16
  private checkpointsKey;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/src/workflow/backends/redis/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/src/workflow/backends/redis/index.ts"],"names":[],"mappings":"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"}
@@ -12,12 +12,25 @@ import { requeueRun } from "../shared/requeue-run.js";
12
12
  import { INITIALIZATION_ERROR, INVALID_ARGUMENT, RESOURCE_NOT_FOUND } from "../../../errors/index.js";
13
13
  import { DenoRedisAdapter, getRedisModule, NodeRedisAdapter, } from "../../../platform/adapters/redis/index.js";
14
14
  const logger = agentLogger.component("redis-backend");
15
+ /**
16
+ * Atomic compare-and-delete: delete the lock only if it still holds our token.
17
+ * Server-side Lua (Redis EVAL) so the GET and DEL are one indivisible step,
18
+ * preventing a stale owner from deleting another worker's reacquired lock.
19
+ */
20
+ const RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
21
+ /**
22
+ * Atomic compare-and-pexpire: extend the lock TTL only if it still holds our
23
+ * token. Same TOCTOU protection as the release script.
24
+ */
25
+ const EXTEND_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
15
26
  /** Implement redis backend. */
16
27
  export class RedisBackend {
17
28
  client = null;
18
29
  connectionPromise = null;
19
30
  config;
20
31
  initialized = false;
32
+ /** Per-run lock tokens for ownership-checked release/extend (Redlock pattern). */
33
+ lockValues = new Map();
21
34
  constructor(config = {}) {
22
35
  this.config = {
23
36
  prefix: "vf:workflow:",
@@ -442,20 +455,40 @@ export class RedisBackend {
442
455
  const client = await this.ensureClient();
443
456
  const lockValue = dntShim.crypto.randomUUID();
444
457
  const result = await client.set(this.lockKey(runId), lockValue, { nx: true, px: duration });
445
- return result === "OK";
458
+ if (result === "OK") {
459
+ // Remember our token so release/extend can verify ownership (Redlock).
460
+ this.lockValues.set(runId, lockValue);
461
+ return true;
462
+ }
463
+ return false;
446
464
  }
447
465
  async releaseLock(runId) {
448
466
  const client = await this.ensureClient();
449
- await client.del(this.lockKey(runId));
467
+ const key = this.lockKey(runId);
468
+ const ourValue = this.lockValues.get(runId);
469
+ // Only release if we still own the lock (compare-and-delete). Without a
470
+ // known token we never owned it, so do nothing.
471
+ if (ourValue === undefined)
472
+ return;
473
+ // Atomic GET + DEL via Lua so a stale owner cannot delete a lock that was
474
+ // reacquired by another worker between the check and the delete (TOCTOU).
475
+ await client.eval(RELEASE_LOCK_SCRIPT, [key], [ourValue]);
476
+ this.lockValues.delete(runId);
450
477
  }
451
478
  async extendLock(runId, duration) {
452
479
  const client = await this.ensureClient();
453
480
  const key = this.lockKey(runId);
454
- const exists = await client.exists(key);
455
- if (exists === 0)
481
+ const ourValue = this.lockValues.get(runId);
482
+ // Only extend if we still own the lock (compare-and-pexpire).
483
+ if (ourValue === undefined)
456
484
  return false;
457
- await client.expire(key, Math.ceil(duration / 1000));
458
- return true;
485
+ // Atomic GET + PEXPIRE via Lua. PEXPIRE returns 1 when the key existed and
486
+ // the TTL was set, 0 otherwise (e.g. our token no longer owns the lock).
487
+ const result = await client.eval(EXTEND_LOCK_SCRIPT, [key], [
488
+ ourValue,
489
+ String(duration),
490
+ ]);
491
+ return Number(result) === 1;
459
492
  }
460
493
  async isLocked(runId) {
461
494
  const client = await this.ensureClient();
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/src/workflow/executor/dag/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAYV,YAAY,EAEZ,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAIxB,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE7F,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAIpB,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAA4B;gBAE9B,MAAM,EAAE,iBAAiB;IAQ/B,OAAO,CACX,KAAK,EAAE,YAAY,EAAE,EACrB,GAAG,EAAE,WAAW,EAChB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,kBAAkB,CAAC;YAsGhB,WAAW;YA8CX,eAAe;YA0Bf,mBAAmB;YA0CnB,cAAc;YAmGd,iBAAiB;YA6DjB,eAAe;YA8Bf,sBAAsB;YA+DtB,eAAe;YA6If,UAAU;CAoBzB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/src/workflow/executor/dag/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAYV,YAAY,EAEZ,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAIxB,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE7F,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAIpB,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAA4B;gBAE9B,MAAM,EAAE,iBAAiB;IAQ/B,OAAO,CACX,KAAK,EAAE,YAAY,EAAE,EACrB,GAAG,EAAE,WAAW,EAChB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,kBAAkB,CAAC;YAsGhB,WAAW;YA8CX,eAAe;YA0Bf,mBAAmB;YA4CnB,cAAc;YAqGd,iBAAiB;YA+DjB,eAAe;YA8Bf,sBAAsB;YA+DtB,eAAe;YAsKf,UAAU;CAoBzB"}
@@ -160,7 +160,9 @@ export class DAGExecutor {
160
160
  workflowId: "",
161
161
  status: "running",
162
162
  input: context.input,
163
- nodeStates: {},
163
+ // Carry already-accumulated child states so completed children are
164
+ // skipped on resume instead of re-executing (H8).
165
+ nodeStates,
164
166
  currentNodes: [],
165
167
  context,
166
168
  checkpoints: [],
@@ -232,7 +234,9 @@ export class DAGExecutor {
232
234
  workflowId: "",
233
235
  status: "running",
234
236
  input: context.input,
235
- nodeStates: {},
237
+ // Carry already-accumulated child states so completed children are
238
+ // skipped on resume instead of re-executing (H8).
239
+ nodeStates,
236
240
  currentNodes: [],
237
241
  context: { ...context },
238
242
  checkpoints: [],
@@ -281,7 +285,9 @@ export class DAGExecutor {
281
285
  workflowId: "",
282
286
  status: "running",
283
287
  input: context.input,
284
- nodeStates: {},
288
+ // Carry already-accumulated child states so completed children are
289
+ // skipped on resume instead of re-executing (H8).
290
+ nodeStates,
285
291
  currentNodes: [],
286
292
  context,
287
293
  checkpoints: [],
@@ -384,9 +390,15 @@ export class DAGExecutor {
384
390
  let exitReason = "condition";
385
391
  let lastError;
386
392
  const existingLoopState = context[`${node.id}_loop_state`];
393
+ // Child node states for the in-flight (resumed) iteration, so its already
394
+ // completed steps are not re-executed on resume (H9).
395
+ let resumeIterationNodeStates;
396
+ let resumeIteration;
387
397
  if (existingLoopState) {
388
398
  iteration = existingLoopState.iteration;
389
399
  previousResults.push(...existingLoopState.previousResults);
400
+ resumeIterationNodeStates = existingLoopState.iterationNodeStates;
401
+ resumeIteration = existingLoopState.iteration;
390
402
  }
391
403
  while (iteration < config.maxIterations) {
392
404
  const loopContext = {
@@ -403,12 +415,19 @@ export class DAGExecutor {
403
415
  const steps = typeof config.steps === "function"
404
416
  ? config.steps(context, loopContext)
405
417
  : config.steps;
418
+ // On resume, rehydrate the in-flight iteration's child node states so its
419
+ // already-completed steps are skipped instead of re-executed (H9).
420
+ const iterationNodeStates = resumeIteration === iteration && resumeIterationNodeStates
421
+ ? { ...resumeIterationNodeStates }
422
+ : {};
423
+ // Only rehydrate once; subsequent iterations start fresh.
424
+ resumeIterationNodeStates = undefined;
406
425
  const result = await this.execute(steps, {
407
426
  id: `${node.id}_iter_${iteration}`,
408
427
  workflowId: "",
409
428
  status: "running",
410
429
  input: context.input,
411
- nodeStates: {},
430
+ nodeStates: iterationNodeStates,
412
431
  currentNodes: [],
413
432
  context: { ...context, _loop: loopContext },
414
433
  checkpoints: [],
@@ -428,7 +447,13 @@ export class DAGExecutor {
428
447
  state,
429
448
  contextUpdates: {
430
449
  ...result.context,
431
- [`${node.id}_loop_state`]: { iteration, previousResults },
450
+ [`${node.id}_loop_state`]: {
451
+ iteration,
452
+ previousResults,
453
+ // Persist the in-flight iteration's child states so completed
454
+ // steps are not re-executed when this iteration resumes (H9).
455
+ iterationNodeStates: result.nodeStates,
456
+ },
432
457
  },
433
458
  waiting: true,
434
459
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.614",
3
+ "version": "0.1.616",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
@@ -73,6 +73,10 @@
73
73
  "import": "./esm/src/agent/index.js",
74
74
  "types": "./esm/src/agent/index.d.ts"
75
75
  },
76
+ "./agent/identity": {
77
+ "import": "./esm/src/agent/identity-contracts.js",
78
+ "types": "./esm/src/agent/identity-contracts.d.ts"
79
+ },
76
80
  "./agent/testing": {
77
81
  "import": "./esm/src/agent/testing/index.js",
78
82
  "types": "./esm/src/agent/testing/index.d.ts"