donobu 5.36.1 → 5.36.2

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 (33) hide show
  1. package/dist/esm/managers/DonobuFlowsManager.d.ts +21 -1
  2. package/dist/esm/managers/DonobuFlowsManager.js +51 -2
  3. package/dist/esm/managers/DonobuStack.js +4 -4
  4. package/dist/esm/managers/FederatedPagination.d.ts +43 -13
  5. package/dist/esm/managers/FederatedPagination.js +122 -39
  6. package/dist/esm/managers/SuitesManager.js +11 -6
  7. package/dist/esm/managers/TestsManager.d.ts +20 -2
  8. package/dist/esm/managers/TestsManager.js +67 -14
  9. package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.js +10 -1
  10. package/dist/esm/persistence/flows/FlowsPersistenceRegistry.d.ts +25 -1
  11. package/dist/esm/persistence/flows/FlowsPersistenceRegistry.js +17 -5
  12. package/dist/esm/persistence/suites/SuitesPersistenceRegistry.d.ts +18 -1
  13. package/dist/esm/persistence/suites/SuitesPersistenceRegistry.js +17 -5
  14. package/dist/esm/persistence/tests/TestsPersistenceRegistry.d.ts +18 -1
  15. package/dist/esm/persistence/tests/TestsPersistenceRegistry.js +20 -5
  16. package/dist/esm/tools/ScrollPageTool.js +1 -1
  17. package/dist/managers/DonobuFlowsManager.d.ts +21 -1
  18. package/dist/managers/DonobuFlowsManager.js +51 -2
  19. package/dist/managers/DonobuStack.js +4 -4
  20. package/dist/managers/FederatedPagination.d.ts +43 -13
  21. package/dist/managers/FederatedPagination.js +122 -39
  22. package/dist/managers/SuitesManager.js +11 -6
  23. package/dist/managers/TestsManager.d.ts +20 -2
  24. package/dist/managers/TestsManager.js +67 -14
  25. package/dist/persistence/flows/FlowsPersistenceDonobuApi.js +10 -1
  26. package/dist/persistence/flows/FlowsPersistenceRegistry.d.ts +25 -1
  27. package/dist/persistence/flows/FlowsPersistenceRegistry.js +17 -5
  28. package/dist/persistence/suites/SuitesPersistenceRegistry.d.ts +18 -1
  29. package/dist/persistence/suites/SuitesPersistenceRegistry.js +17 -5
  30. package/dist/persistence/tests/TestsPersistenceRegistry.d.ts +18 -1
  31. package/dist/persistence/tests/TestsPersistenceRegistry.js +20 -5
  32. package/dist/tools/ScrollPageTool.js +1 -1
  33. package/package.json +1 -1
@@ -125,7 +125,16 @@ class FlowsPersistenceDonobuApi {
125
125
  params.set('orphaned', query.orphaned ? 'true' : 'false');
126
126
  }
127
127
  if (query.sortBy) {
128
- params.set('sort_by', query.sortBy);
128
+ switch (query.sortBy) {
129
+ case 'created_at':
130
+ // `donobu-api` only supports `started_at`, but that's essentially
131
+ // equivalent to `created_at`
132
+ params.set('sort_by', 'started_at');
133
+ break;
134
+ default:
135
+ params.set('sort_by', query.sortBy);
136
+ break;
137
+ }
129
138
  }
130
139
  if (query.sortOrder) {
131
140
  params.set('sort_order', query.sortOrder);
@@ -2,6 +2,17 @@ import type { EnvPick } from 'env-struct';
2
2
  import type { env } from '../../envVars';
3
3
  import { PersistencePluginRegistry } from '../PersistencePlugin';
4
4
  import type { FlowsPersistence } from './FlowsPersistence';
5
+ /**
6
+ * A persistence layer paired with the `PERSISTENCE_PRIORITY` key it was
7
+ * created from. The key is shared across the flows/tests/suites registries:
8
+ * the same key in each registry refers to the same logical store, which is
9
+ * how cross-entity foreign keys are kept aligned (e.g. a flow's `testId`
10
+ * resolves to a test that lives in the same layer).
11
+ */
12
+ export interface FlowsPersistenceLayer {
13
+ key: string;
14
+ persistence: FlowsPersistence;
15
+ }
5
16
  export interface FlowsPersistenceRegistry {
6
17
  /**
7
18
  * Returns the primary persistence layer.
@@ -9,6 +20,17 @@ export interface FlowsPersistenceRegistry {
9
20
  get(): Promise<FlowsPersistence>;
10
21
  /** Returns a list of all valid persistence layers. Guaranteed to be non-empty. */
11
22
  getAll(): Promise<FlowsPersistence[]>;
23
+ /**
24
+ * Returns all valid layers paired with their `PERSISTENCE_PRIORITY` key.
25
+ * Used by callers that need to align writes across registries (e.g. write
26
+ * a flow into the same layer as the test it references).
27
+ */
28
+ getEntries(): Promise<FlowsPersistenceLayer[]>;
29
+ /**
30
+ * Returns the persistence layer registered under the given
31
+ * `PERSISTENCE_PRIORITY` key, or undefined if no such layer exists.
32
+ */
33
+ getByKey(key: string): Promise<FlowsPersistence | undefined>;
12
34
  }
13
35
  /**
14
36
  * A factory class for creating FlowsPersistence instances. Persistence layers are constructed
@@ -19,7 +41,7 @@ export declare class FlowsPersistenceRegistryImpl implements FlowsPersistenceReg
19
41
  /**
20
42
  * Creates an instance with pre-built persistence layers.
21
43
  */
22
- constructor(layers: FlowsPersistence[]);
44
+ constructor(layers: FlowsPersistenceLayer[]);
23
45
  /**
24
46
  * Creates an instance by reading environment variables and eagerly constructing
25
47
  * all applicable persistence layers.
@@ -31,5 +53,7 @@ export declare class FlowsPersistenceRegistryImpl implements FlowsPersistenceReg
31
53
  get(): Promise<FlowsPersistence>;
32
54
  /** Returns all persistence layers. Guaranteed to be non-empty. */
33
55
  getAll(): Promise<FlowsPersistence[]>;
56
+ getEntries(): Promise<FlowsPersistenceLayer[]>;
57
+ getByKey(key: string): Promise<FlowsPersistence | undefined>;
34
58
  }
35
59
  //# sourceMappingURL=FlowsPersistenceRegistry.d.ts.map
@@ -32,21 +32,27 @@ class FlowsPersistenceRegistryImpl {
32
32
  switch (key) {
33
33
  case 'DONOBU':
34
34
  if (donobuApiKey && donobuApiBaseUrl) {
35
- layers.push(new FlowsPersistenceDonobuApi_1.FlowsPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey));
35
+ layers.push({
36
+ key,
37
+ persistence: new FlowsPersistenceDonobuApi_1.FlowsPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey),
38
+ });
36
39
  }
37
40
  break;
38
41
  case 'LOCAL':
39
- layers.push(await FlowsPersistenceSqlite_1.FlowsPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()));
42
+ layers.push({
43
+ key,
44
+ persistence: await FlowsPersistenceSqlite_1.FlowsPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()),
45
+ });
40
46
  break;
41
47
  case 'RAM':
42
- layers.push(new FlowsPersistenceVolatile_1.FlowsPersistenceVolatile());
48
+ layers.push({ key, persistence: new FlowsPersistenceVolatile_1.FlowsPersistenceVolatile() });
43
49
  break;
44
50
  default: {
45
51
  const plugin = persistencePlugins.get(key);
46
52
  if (plugin) {
47
53
  const impl = await plugin.createFlowsPersistence();
48
54
  if (impl) {
49
- layers.push(impl);
55
+ layers.push({ key, persistence: impl });
50
56
  }
51
57
  }
52
58
  break;
@@ -59,12 +65,18 @@ class FlowsPersistenceRegistryImpl {
59
65
  * Returns the primary persistence layer.
60
66
  */
61
67
  async get() {
62
- return this.layers[0];
68
+ return this.layers[0].persistence;
63
69
  }
64
70
  /** Returns all persistence layers. Guaranteed to be non-empty. */
65
71
  async getAll() {
72
+ return this.layers.map((layer) => layer.persistence);
73
+ }
74
+ async getEntries() {
66
75
  return this.layers;
67
76
  }
77
+ async getByKey(key) {
78
+ return this.layers.find((layer) => layer.key === key)?.persistence;
79
+ }
68
80
  }
69
81
  exports.FlowsPersistenceRegistryImpl = FlowsPersistenceRegistryImpl;
70
82
  //# sourceMappingURL=FlowsPersistenceRegistry.js.map
@@ -2,17 +2,34 @@ import type { EnvPick } from 'env-struct';
2
2
  import type { env } from '../../envVars';
3
3
  import { PersistencePluginRegistry } from '../PersistencePlugin';
4
4
  import type { SuitesPersistence } from './SuitesPersistence';
5
+ /**
6
+ * A persistence layer paired with its `PERSISTENCE_PRIORITY` key. See
7
+ * {@link FlowsPersistenceLayer} for why keys must align across registries.
8
+ */
9
+ export interface SuitesPersistenceLayer {
10
+ key: string;
11
+ persistence: SuitesPersistence;
12
+ }
5
13
  export interface SuitesPersistenceRegistry {
6
14
  /** Returns the primary persistence layer. */
7
15
  get(): Promise<SuitesPersistence>;
8
16
  /** Returns all valid persistence layers. Guaranteed to be non-empty. */
9
17
  getAll(): Promise<SuitesPersistence[]>;
18
+ /** Returns all valid layers paired with their `PERSISTENCE_PRIORITY` key. */
19
+ getEntries(): Promise<SuitesPersistenceLayer[]>;
20
+ /**
21
+ * Returns the persistence layer registered under the given
22
+ * `PERSISTENCE_PRIORITY` key, or undefined if no such layer exists.
23
+ */
24
+ getByKey(key: string): Promise<SuitesPersistence | undefined>;
10
25
  }
11
26
  export declare class SuitesPersistenceRegistryImpl implements SuitesPersistenceRegistry {
12
27
  private readonly layers;
13
- constructor(layers: SuitesPersistence[]);
28
+ constructor(layers: SuitesPersistenceLayer[]);
14
29
  static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'PERSISTENCE_PRIORITY'>, persistencePlugins?: PersistencePluginRegistry): Promise<SuitesPersistenceRegistryImpl>;
15
30
  get(): Promise<SuitesPersistence>;
16
31
  getAll(): Promise<SuitesPersistence[]>;
32
+ getEntries(): Promise<SuitesPersistenceLayer[]>;
33
+ getByKey(key: string): Promise<SuitesPersistence | undefined>;
17
34
  }
18
35
  //# sourceMappingURL=SuitesPersistenceRegistry.d.ts.map
@@ -21,21 +21,27 @@ class SuitesPersistenceRegistryImpl {
21
21
  switch (key) {
22
22
  case 'DONOBU':
23
23
  if (donobuApiKey && donobuApiBaseUrl) {
24
- layers.push(new SuitesPersistenceDonobuApi_1.SuitesPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey));
24
+ layers.push({
25
+ key,
26
+ persistence: new SuitesPersistenceDonobuApi_1.SuitesPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey),
27
+ });
25
28
  }
26
29
  break;
27
30
  case 'LOCAL':
28
- layers.push(await SuitesPersistenceSqlite_1.SuitesPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()));
31
+ layers.push({
32
+ key,
33
+ persistence: await SuitesPersistenceSqlite_1.SuitesPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()),
34
+ });
29
35
  break;
30
36
  case 'RAM':
31
- layers.push(new SuitesPersistenceVolatile_1.SuitesPersistenceVolatile());
37
+ layers.push({ key, persistence: new SuitesPersistenceVolatile_1.SuitesPersistenceVolatile() });
32
38
  break;
33
39
  default: {
34
40
  const plugin = persistencePlugins.get(key);
35
41
  if (plugin?.createSuitesPersistence) {
36
42
  const impl = await plugin.createSuitesPersistence();
37
43
  if (impl) {
38
- layers.push(impl);
44
+ layers.push({ key, persistence: impl });
39
45
  }
40
46
  }
41
47
  break;
@@ -45,11 +51,17 @@ class SuitesPersistenceRegistryImpl {
45
51
  return new SuitesPersistenceRegistryImpl(layers);
46
52
  }
47
53
  async get() {
48
- return this.layers[0];
54
+ return this.layers[0].persistence;
49
55
  }
50
56
  async getAll() {
57
+ return this.layers.map((layer) => layer.persistence);
58
+ }
59
+ async getEntries() {
51
60
  return this.layers;
52
61
  }
62
+ async getByKey(key) {
63
+ return this.layers.find((layer) => layer.key === key)?.persistence;
64
+ }
53
65
  }
54
66
  exports.SuitesPersistenceRegistryImpl = SuitesPersistenceRegistryImpl;
55
67
  //# sourceMappingURL=SuitesPersistenceRegistry.js.map
@@ -3,15 +3,30 @@ import type { env } from '../../envVars';
3
3
  import type { FlowsPersistenceRegistry } from '../flows/FlowsPersistenceRegistry';
4
4
  import { PersistencePluginRegistry } from '../PersistencePlugin';
5
5
  import type { TestsPersistence } from './TestsPersistence';
6
+ /**
7
+ * A persistence layer paired with its `PERSISTENCE_PRIORITY` key. See
8
+ * {@link FlowsPersistenceLayer} for why keys must align across registries.
9
+ */
10
+ export interface TestsPersistenceLayer {
11
+ key: string;
12
+ persistence: TestsPersistence;
13
+ }
6
14
  export interface TestsPersistenceRegistry {
7
15
  /** Returns the primary persistence layer. */
8
16
  get(): Promise<TestsPersistence>;
9
17
  /** Returns all valid persistence layers. Guaranteed to be non-empty. */
10
18
  getAll(): Promise<TestsPersistence[]>;
19
+ /** Returns all valid layers paired with their `PERSISTENCE_PRIORITY` key. */
20
+ getEntries(): Promise<TestsPersistenceLayer[]>;
21
+ /**
22
+ * Returns the persistence layer registered under the given
23
+ * `PERSISTENCE_PRIORITY` key, or undefined if no such layer exists.
24
+ */
25
+ getByKey(key: string): Promise<TestsPersistence | undefined>;
11
26
  }
12
27
  export declare class TestsPersistenceRegistryImpl implements TestsPersistenceRegistry {
13
28
  private readonly layers;
14
- constructor(layers: TestsPersistence[]);
29
+ constructor(layers: TestsPersistenceLayer[]);
15
30
  static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'PERSISTENCE_PRIORITY'>,
16
31
  /**
17
32
  * Flows registry — used by the volatile tests layer to compute
@@ -22,5 +37,7 @@ export declare class TestsPersistenceRegistryImpl implements TestsPersistenceReg
22
37
  flowsRegistry?: FlowsPersistenceRegistry, persistencePlugins?: PersistencePluginRegistry): Promise<TestsPersistenceRegistryImpl>;
23
38
  get(): Promise<TestsPersistence>;
24
39
  getAll(): Promise<TestsPersistence[]>;
40
+ getEntries(): Promise<TestsPersistenceLayer[]>;
41
+ getByKey(key: string): Promise<TestsPersistence | undefined>;
25
42
  }
26
43
  //# sourceMappingURL=TestsPersistenceRegistry.d.ts.map
@@ -28,21 +28,30 @@ class TestsPersistenceRegistryImpl {
28
28
  switch (key) {
29
29
  case 'DONOBU':
30
30
  if (donobuApiKey && donobuApiBaseUrl) {
31
- layers.push(new TestsPersistenceDonobuApi_1.TestsPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey));
31
+ layers.push({
32
+ key,
33
+ persistence: new TestsPersistenceDonobuApi_1.TestsPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey),
34
+ });
32
35
  }
33
36
  break;
34
37
  case 'LOCAL':
35
- layers.push(await TestsPersistenceSqlite_1.TestsPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()));
38
+ layers.push({
39
+ key,
40
+ persistence: await TestsPersistenceSqlite_1.TestsPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()),
41
+ });
36
42
  break;
37
43
  case 'RAM':
38
- layers.push(new TestsPersistenceVolatile_1.TestsPersistenceVolatile(undefined, flowsRegistry ? () => flowsRegistry.get() : undefined));
44
+ layers.push({
45
+ key,
46
+ persistence: new TestsPersistenceVolatile_1.TestsPersistenceVolatile(undefined, flowsRegistry ? () => flowsRegistry.get() : undefined),
47
+ });
39
48
  break;
40
49
  default: {
41
50
  const plugin = persistencePlugins.get(key);
42
51
  if (plugin?.createTestsPersistence) {
43
52
  const impl = await plugin.createTestsPersistence();
44
53
  if (impl) {
45
- layers.push(impl);
54
+ layers.push({ key, persistence: impl });
46
55
  }
47
56
  }
48
57
  break;
@@ -52,11 +61,17 @@ class TestsPersistenceRegistryImpl {
52
61
  return new TestsPersistenceRegistryImpl(layers);
53
62
  }
54
63
  async get() {
55
- return this.layers[0];
64
+ return this.layers[0].persistence;
56
65
  }
57
66
  async getAll() {
67
+ return this.layers.map((layer) => layer.persistence);
68
+ }
69
+ async getEntries() {
58
70
  return this.layers;
59
71
  }
72
+ async getByKey(key) {
73
+ return this.layers.find((layer) => layer.key === key)?.persistence;
74
+ }
60
75
  }
61
76
  exports.TestsPersistenceRegistryImpl = TestsPersistenceRegistryImpl;
62
77
  //# sourceMappingURL=TestsPersistenceRegistry.js.map
@@ -74,7 +74,7 @@ current page or element, of which may contain relevant information.`, exports.Sc
74
74
  return beforeY !== afterY || beforeX !== afterX;
75
75
  }, parameters);
76
76
  return didScroll
77
- ? `Scrolled ${parameters.direction} ${parameters.maxScroll ? ' (max)' : ''}`
77
+ ? `Scrolled ${parameters.direction} ${parameters.maxScroll ? '(maximum amount scrolled)' : ''} for `
78
78
  : `Nothing to scroll ${parameters.direction}`;
79
79
  }
80
80
  }
@@ -22,6 +22,7 @@ import type { RunConfig } from '../models/RunConfig';
22
22
  import type { RunMode } from '../models/RunMode';
23
23
  import type { ToolCall } from '../models/ToolCall';
24
24
  import type { FlowsPersistenceRegistry } from '../persistence/flows/FlowsPersistenceRegistry';
25
+ import type { TestsPersistenceRegistry } from '../persistence/tests/TestsPersistenceRegistry';
25
26
  import type { TargetRuntimePluginRegistry } from '../targets/TargetRuntimePlugin';
26
27
  import type { Tool } from '../tools/Tool';
27
28
  import type { FlowLogSnapshot } from '../utils/FlowLogBuffer';
@@ -45,17 +46,22 @@ export declare class DonobuFlowsManager {
45
46
  private readonly environ;
46
47
  private readonly toolRegistry;
47
48
  private readonly targetRuntimePlugins;
49
+ private readonly testsPersistenceRegistry;
48
50
  static readonly DEFAULT_MESSAGE_DURATION = 2247;
49
51
  static readonly DEFAULT_MAX_TOOL_CALLS = 50;
50
52
  static readonly DEFAULT_BROWSER_STATE_FILENAME = "browserstate.json";
51
53
  static readonly FLOW_NAME_MAX_LENGTH = 255;
52
54
  private readonly flowRuntime;
53
55
  private readonly flowCatalog;
54
- constructor(deploymentEnvironment: DonobuDeploymentEnvironment, gptClientFactory: GptClientFactory, gptConfigsManager: GptConfigsManager, agentsManager: AgentsManager, flowsPersistenceRegistry: FlowsPersistenceRegistry, envDataManager: EnvDataManager, controlPanelFactory: ControlPanelFactory, environ: EnvPick<typeof env, 'ANTHROPIC_API_KEY' | 'ANTHROPIC_MODEL_NAME' | 'AWS_ACCESS_KEY_ID' | 'AWS_BEDROCK_MODEL_NAME' | 'AWS_SECRET_ACCESS_KEY' | 'BASE64_GPT_CONFIG' | 'BROWSERBASE_API_KEY' | 'BROWSERBASE_PROJECT_ID' | 'DONOBU_API_KEY' | 'GOOGLE_GENERATIVE_AI_API_KEY' | 'GOOGLE_GENERATIVE_AI_MODEL_NAME' | 'OLLAMA_API_URL' | 'OLLAMA_MODEL_NAME' | 'OPENAI_API_KEY' | 'OPENAI_API_MODEL_NAME'>, toolRegistry: ToolRegistry, targetRuntimePlugins: TargetRuntimePluginRegistry);
56
+ constructor(deploymentEnvironment: DonobuDeploymentEnvironment, gptClientFactory: GptClientFactory, gptConfigsManager: GptConfigsManager, agentsManager: AgentsManager, flowsPersistenceRegistry: FlowsPersistenceRegistry, envDataManager: EnvDataManager, controlPanelFactory: ControlPanelFactory, environ: EnvPick<typeof env, 'ANTHROPIC_API_KEY' | 'ANTHROPIC_MODEL_NAME' | 'AWS_ACCESS_KEY_ID' | 'AWS_BEDROCK_MODEL_NAME' | 'AWS_SECRET_ACCESS_KEY' | 'BASE64_GPT_CONFIG' | 'BROWSERBASE_API_KEY' | 'BROWSERBASE_PROJECT_ID' | 'DONOBU_API_KEY' | 'GOOGLE_GENERATIVE_AI_API_KEY' | 'GOOGLE_GENERATIVE_AI_MODEL_NAME' | 'OLLAMA_API_URL' | 'OLLAMA_MODEL_NAME' | 'OPENAI_API_KEY' | 'OPENAI_API_MODEL_NAME'>, toolRegistry: ToolRegistry, targetRuntimePlugins: TargetRuntimePluginRegistry, testsPersistenceRegistry: TestsPersistenceRegistry);
55
57
  /**
56
58
  * Create a flow with the given parameters and invoke its `DonobuFlow#run`
57
59
  * method, adding it to list of active flows.
58
60
  *
61
+ * If `flowParams.testId` is set, the new flow is persisted to the same
62
+ * layer as the referenced test so the `flow_metadata.test_id` foreign
63
+ * key holds. Otherwise the primary layer is used.
64
+ *
59
65
  * @param gptClient If present, will use this as the associated GPT client for
60
66
  * this flow instead of instantiating a new one. If so, the
61
67
  * gptConfigNameOverride field will be ignored.
@@ -168,6 +174,20 @@ export declare class DonobuFlowsManager {
168
174
  * {@link BrowserStateNotFoundException} if it is not found.
169
175
  */
170
176
  getBrowserStorageState(browserStateRef: BrowserStateReference): Promise<BrowserStorageState>;
177
+ /**
178
+ * Picks the flows persistence layer to use when creating a new flow.
179
+ *
180
+ * - If `testId` is null/undefined: use the primary flows layer.
181
+ * - If `testId` is set: look up the test's layer key and use the matching
182
+ * flows layer. If no flows layer matches the test's key (rare —
183
+ * asymmetric registry config), fall back to the primary layer; the FK
184
+ * won't hold but the flow is at least persisted.
185
+ * - If `testId` is set but no test exists with that ID anywhere: fall
186
+ * back to the primary layer (the SQLite FK will reject if applicable;
187
+ * non-DB layers will accept the dangling reference).
188
+ */
189
+ private resolveLayerForFlowCreate;
190
+ private findTestLayerKey;
171
191
  private isLocallyRunning;
172
192
  }
173
193
  /**
@@ -47,6 +47,7 @@ const BrowserStateNotFoundException_1 = require("../exceptions/BrowserStateNotFo
47
47
  const CannotDeleteRunningFlowException_1 = require("../exceptions/CannotDeleteRunningFlowException");
48
48
  const FlowNotFoundException_1 = require("../exceptions/FlowNotFoundException");
49
49
  const InvalidParamValueException_1 = require("../exceptions/InvalidParamValueException");
50
+ const TestNotFoundException_1 = require("../exceptions/TestNotFoundException");
50
51
  const ToolRequiresGptException_1 = require("../exceptions/ToolRequiresGptException");
51
52
  const UnknownToolException_1 = require("../exceptions/UnknownToolException");
52
53
  const GptConfig_1 = require("../models/GptConfig");
@@ -64,7 +65,7 @@ const FlowRuntime_1 = require("./FlowRuntime");
64
65
  const InteractionVisualizer_1 = require("./InteractionVisualizer");
65
66
  const ToolManager_1 = require("./ToolManager");
66
67
  class DonobuFlowsManager {
67
- constructor(deploymentEnvironment, gptClientFactory, gptConfigsManager, agentsManager, flowsPersistenceRegistry, envDataManager, controlPanelFactory, environ, toolRegistry, targetRuntimePlugins) {
68
+ constructor(deploymentEnvironment, gptClientFactory, gptConfigsManager, agentsManager, flowsPersistenceRegistry, envDataManager, controlPanelFactory, environ, toolRegistry, targetRuntimePlugins, testsPersistenceRegistry) {
68
69
  this.deploymentEnvironment = deploymentEnvironment;
69
70
  this.gptClientFactory = gptClientFactory;
70
71
  this.gptConfigsManager = gptConfigsManager;
@@ -75,6 +76,7 @@ class DonobuFlowsManager {
75
76
  this.environ = environ;
76
77
  this.toolRegistry = toolRegistry;
77
78
  this.targetRuntimePlugins = targetRuntimePlugins;
79
+ this.testsPersistenceRegistry = testsPersistenceRegistry;
78
80
  this.flowRuntime = new FlowRuntime_1.FlowRuntime();
79
81
  this.flowCatalog = new FlowCatalog_1.FlowCatalog(this.flowsPersistenceRegistry, this.flowRuntime, this.deploymentEnvironment);
80
82
  }
@@ -82,6 +84,10 @@ class DonobuFlowsManager {
82
84
  * Create a flow with the given parameters and invoke its `DonobuFlow#run`
83
85
  * method, adding it to list of active flows.
84
86
  *
87
+ * If `flowParams.testId` is set, the new flow is persisted to the same
88
+ * layer as the referenced test so the `flow_metadata.test_id` foreign
89
+ * key holds. Otherwise the primary layer is used.
90
+ *
85
91
  * @param gptClient If present, will use this as the associated GPT client for
86
92
  * this flow instead of instantiating a new one. If so, the
87
93
  * gptConfigNameOverride field will be ignored.
@@ -179,7 +185,7 @@ class DonobuFlowsManager {
179
185
  ...targetRuntime.getMetadataFields(),
180
186
  provenance: (0, buildProvenance_1.buildProvenance)('DONOBU_STUDIO'),
181
187
  };
182
- const flowsPersistence = await this.flowsPersistenceRegistry.get();
188
+ const flowsPersistence = await this.resolveLayerForFlowCreate(flowParams.testId ?? null);
183
189
  const envData = await this.envDataManager.getByNames(flowMetadata.envVars ?? []);
184
190
  const donobuFlow = new DonobuFlow_1.DonobuFlow(this, envData, flowsPersistence, gptClientData.gptClient, toolManager, interactionVisualizer, toolCallsOnStart, [], [], targetRuntime.inspector, flowMetadata, targetRuntime.controlPanel);
185
191
  await flowsPersistence.setFlowMetadata(flowMetadata);
@@ -652,6 +658,49 @@ class DonobuFlowsManager {
652
658
  }
653
659
  throw new BrowserStateNotFoundException_1.BrowserStateNotFoundException(flowMetadata.id);
654
660
  }
661
+ /**
662
+ * Picks the flows persistence layer to use when creating a new flow.
663
+ *
664
+ * - If `testId` is null/undefined: use the primary flows layer.
665
+ * - If `testId` is set: look up the test's layer key and use the matching
666
+ * flows layer. If no flows layer matches the test's key (rare —
667
+ * asymmetric registry config), fall back to the primary layer; the FK
668
+ * won't hold but the flow is at least persisted.
669
+ * - If `testId` is set but no test exists with that ID anywhere: fall
670
+ * back to the primary layer (the SQLite FK will reject if applicable;
671
+ * non-DB layers will accept the dangling reference).
672
+ */
673
+ async resolveLayerForFlowCreate(testId) {
674
+ if (!testId) {
675
+ return this.flowsPersistenceRegistry.get();
676
+ }
677
+ let testLayerKey;
678
+ try {
679
+ testLayerKey = await this.findTestLayerKey(testId);
680
+ }
681
+ catch (error) {
682
+ if (!(error instanceof TestNotFoundException_1.TestNotFoundException)) {
683
+ throw error;
684
+ }
685
+ return this.flowsPersistenceRegistry.get();
686
+ }
687
+ const matched = await this.flowsPersistenceRegistry.getByKey(testLayerKey);
688
+ return matched ?? (await this.flowsPersistenceRegistry.get());
689
+ }
690
+ async findTestLayerKey(testId) {
691
+ for (const { key, persistence, } of await this.testsPersistenceRegistry.getEntries()) {
692
+ try {
693
+ await persistence.getTestById(testId);
694
+ return key;
695
+ }
696
+ catch (error) {
697
+ if (!(error instanceof TestNotFoundException_1.TestNotFoundException)) {
698
+ throw error;
699
+ }
700
+ }
701
+ }
702
+ throw TestNotFoundException_1.TestNotFoundException.forId(testId);
703
+ }
655
704
  isLocallyRunning() {
656
705
  return this.deploymentEnvironment === 'LOCAL';
657
706
  }
@@ -46,12 +46,12 @@ async function setupDonobuStack(donobuDeploymentEnvironment, controlPanelFactory
46
46
  const agentsPersistence = await AgentsPersistenceSqlite_1.AgentsPersistenceSqlite.create(sqliteDb);
47
47
  const agentsManager = await AgentsManager_1.AgentsManager.create(agentsPersistence, gptConfigsManager);
48
48
  const flowsPersistenceRegistry = await FlowsPersistenceRegistry_1.FlowsPersistenceRegistryImpl.fromEnvironment(environ, persistencePlugins);
49
- const envPersistenceFactory = await EnvPersistenceRegistry_1.EnvPersistenceRegistryImpl.fromEnvironment(envPersistenceVolatile ?? null, environ, persistencePlugins);
50
- const envDataManager = new EnvDataManager_1.EnvDataManager(envPersistenceFactory);
51
- const flowsManager = new DonobuFlowsManager_1.DonobuFlowsManager(donobuDeploymentEnvironment, gptClientFactory, gptConfigsManager, agentsManager, flowsPersistenceRegistry, envDataManager, controlPanelFactory, environ, resolvedToolRegistry, targetRuntimePlugins);
52
49
  const testsPersistenceRegistry = await TestsPersistenceRegistry_1.TestsPersistenceRegistryImpl.fromEnvironment(environ, flowsPersistenceRegistry);
53
- const testsManager = new TestsManager_1.TestsManager(testsPersistenceRegistry, flowsManager);
54
50
  const suitesPersistenceRegistry = await SuitesPersistenceRegistry_1.SuitesPersistenceRegistryImpl.fromEnvironment(environ);
51
+ const envPersistenceFactory = await EnvPersistenceRegistry_1.EnvPersistenceRegistryImpl.fromEnvironment(envPersistenceVolatile ?? null, environ, persistencePlugins);
52
+ const envDataManager = new EnvDataManager_1.EnvDataManager(envPersistenceFactory);
53
+ const flowsManager = new DonobuFlowsManager_1.DonobuFlowsManager(donobuDeploymentEnvironment, gptClientFactory, gptConfigsManager, agentsManager, flowsPersistenceRegistry, envDataManager, controlPanelFactory, environ, resolvedToolRegistry, targetRuntimePlugins, testsPersistenceRegistry);
54
+ const testsManager = new TestsManager_1.TestsManager(testsPersistenceRegistry, suitesPersistenceRegistry, flowsManager);
55
55
  const suitesManager = new SuitesManager_1.SuitesManager(suitesPersistenceRegistry, testsPersistenceRegistry);
56
56
  return {
57
57
  toolRegistry: resolvedToolRegistry,
@@ -1,31 +1,61 @@
1
1
  import type { PaginatedResult } from '../models/PaginatedResult';
2
+ /**
3
+ * Per-layer pagination state, carried across pages inside the composite
4
+ * page token. `cursor` is the layer's own opaque page token; `resumeAfterId`
5
+ * is the id of the last item from this layer kept on the previous page (in
6
+ * the layer's own order). `exhausted` becomes true once a layer has nothing
7
+ * more to give and we've already returned everything it contributed.
8
+ */
9
+ interface SourceState {
10
+ cursor?: string;
11
+ resumeAfterId?: string;
12
+ exhausted: boolean;
13
+ }
2
14
  /**
3
15
  * Internal state tracked across pages when federating results from multiple
4
- * persistence layers.
16
+ * persistence layers. Serialized as base64(JSON(...)) into the composite
17
+ * page token.
5
18
  */
6
19
  interface FederatedPaginationState {
7
- sourceTokens: Record<number, string>;
8
- exhaustedSources: number[];
9
- cursorTimestamp: number | null;
20
+ sources: SourceState[];
10
21
  }
11
22
  export declare function createCompositePageToken(state: FederatedPaginationState): string;
12
23
  export declare function parseCompositePageToken(token?: string): FederatedPaginationState;
13
24
  /**
14
- * Federate a paginated listing across multiple persistence layers.
25
+ * Federate a paginated listing across multiple persistence layers
26
+ * (resume-by-id).
27
+ *
28
+ * Each layer holds an opaque cursor and an optional `resumeAfterId`. On
29
+ * each page request, federation fetches from each non-exhausted layer
30
+ * (advancing through batches as needed until it has at least `limit`
31
+ * candidates per layer), stable-sorts the merged contributions with the
32
+ * caller's `comparator`, and trims to `limit`. For each layer, the
33
+ * **last kept item in that layer's order** becomes the new
34
+ * `resumeAfterId`, with `cursor` set to the layer-token used to fetch
35
+ * that item's batch. Layers whose contribution is wholly dropped by the
36
+ * trim retain their pre-call state so their items remain reachable on
37
+ * later pages.
15
38
  *
16
- * Each layer is queried with up to the requested limit, results are merged,
17
- * sorted by the provided comparator, and trimmed to the requested limit.
18
- * A composite page token tracks per-source pagination state.
39
+ * **Critical contract: the comparator must NOT tiebreak by id.** Ties
40
+ * on the sort key must return 0 from `comparator`. `Array.sort` is
41
+ * stable since ES2019, and federation feeds the merge in
42
+ * `[layer0..., layer1..., ...]` insertion order, so ties resolve by
43
+ * `(layerIndex, layer-internal position)` — exactly the property that
44
+ * keeps each layer's kept items as a *prefix* of its own order, which
45
+ * is what makes a single `resumeAfterId` representable. An id
46
+ * tiebreaker would silently break this and lose items.
19
47
  *
20
- * @param layers The persistence layers to query.
21
- * @param query The query object (must have `limit`, `pageToken`).
22
- * @param fetcher Function that queries a single layer.
23
- * @param comparator Sort comparator for merging results across layers.
48
+ * **Layer contract.** Layers must return the same items in the same
49
+ * order for the same `(query, pageToken)` pair across calls. Their
50
+ * internal ordering does not need to match federation's tiebreaker —
51
+ * just be deterministic.
24
52
  */
25
53
  export declare function federatedList<TQuery extends {
26
54
  limit?: number;
27
55
  pageToken?: string;
28
- }, TItem>(layers: {
56
+ }, TItem extends {
57
+ id: string;
58
+ }>(layers: {
29
59
  getItems: (query: TQuery) => Promise<PaginatedResult<TItem>>;
30
60
  }[], query: TQuery, comparator: (a: TItem, b: TItem) => number): Promise<PaginatedResult<TItem>>;
31
61
  export {};