@oomol-lab/open-flow 0.1.0-beta.78 → 0.1.0-beta.79
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/dist/browser/{createResourceDialog-DCGxfKFM.js → createResourceDialog-DkovGogv.js} +2 -2
- package/dist/browser/{dialog-WzR4CRFH.js → dialog-CFFuRM8s.js} +27 -6
- package/dist/browser/{flowChanges-Bz4dbDN_.js → flowChanges-CN2R-m1M.js} +59 -40
- package/dist/browser/{flowWorkspace-BI6L6a4r.js → flowWorkspace-CXVL1FNv.js} +85 -39
- package/dist/browser/preview.js +122 -48
- package/dist/browser/{typeScriptSession-CRoezJPN.js → typeScriptSession-CoEIptNC.js} +1 -1
- package/dist/browser/workbench.js +4 -4
- package/dist/browser/{workbenchSelect-DtAd0zc4.js → workbenchSelect-CkOS95gX.js} +1 -1
- package/dist/command/index.js +29 -29
- package/dist/common/control-api-conformance.js +80 -7
- package/dist/common/control-api.d.ts +3 -2
- package/dist/common/control-api.js +40 -7
- package/dist/common/control-requests.d.ts +5 -0
- package/dist/common/control-requests.js +28 -5
- package/dist/common/flow-authoring.js +26 -5
- package/dist/common/flow-change.js +26 -5
- package/dist/common/flow-encoding.js +26 -5
- package/dist/common/flow-semantics.js +26 -5
- package/dist/common/flow-state.d.ts +33 -4
- package/dist/common/flow-state.js +49 -6
- package/dist/common/mcp.d.ts +32 -1
- package/dist/common/mcp.js +40 -8
- package/dist/common/runtime-contract.d.ts +4 -1
- package/dist/common/runtime-contract.js +60 -5
- package/dist/common/scheduler.js +26 -5
- package/dist/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/control/common/api.ts +16 -4
- package/src/control/common/conformance.ts +31 -0
- package/src/control/common/mcp.ts +7 -2
- package/src/control/common/requests.ts +3 -0
- package/src/runtime/common/runtime.ts +39 -5
- package/src/state/common/conformance.ts +16 -0
- package/src/state/common/state.ts +41 -8
- package/src/types/index.ts +1 -0
|
@@ -5603,6 +5603,30 @@ function dequal(foo, bar) {
|
|
|
5603
5603
|
//#endregion
|
|
5604
5604
|
//#region src/state/common/conformance.ts
|
|
5605
5605
|
var stateConformanceCases = [
|
|
5606
|
+
{
|
|
5607
|
+
name: "reads selected State keys in input order with missing, falsy and independent values",
|
|
5608
|
+
async verify(harness) {
|
|
5609
|
+
if (!dequal(await harness.run(`export default async (_, context) => {
|
|
5610
|
+
await context.state.set('object', { nested: [1] });
|
|
5611
|
+
await context.state.set('zero', 0);
|
|
5612
|
+
await context.state.set('false', false);
|
|
5613
|
+
await context.state.set('empty', '');
|
|
5614
|
+
const values = await context.state.get(['false', 'missing', 'object', 'zero', 'empty']);
|
|
5615
|
+
values[2].nested.push(2);
|
|
5616
|
+
return {result: {values, retained: await context.state.get('object'), zero: await context.state.get('zero')}};
|
|
5617
|
+
}`), {
|
|
5618
|
+
values: [
|
|
5619
|
+
false,
|
|
5620
|
+
null,
|
|
5621
|
+
{ nested: [1, 2] },
|
|
5622
|
+
0,
|
|
5623
|
+
""
|
|
5624
|
+
],
|
|
5625
|
+
retained: { nested: [1] },
|
|
5626
|
+
zero: 0
|
|
5627
|
+
})) throw new Error("State batch reads must preserve order, missing keys and independent JSON values.");
|
|
5628
|
+
}
|
|
5629
|
+
},
|
|
5606
5630
|
{
|
|
5607
5631
|
name: "skips composite identities across successful Runs without depending on field order",
|
|
5608
5632
|
async verify(harness) {
|
|
@@ -5700,13 +5724,22 @@ var stateConformanceCases = [
|
|
|
5700
5724
|
];
|
|
5701
5725
|
//#endregion
|
|
5702
5726
|
//#region src/state/common/state.ts
|
|
5727
|
+
var stateReadLimits = {
|
|
5728
|
+
maxKeys: 16,
|
|
5729
|
+
maxBytes: 1048576
|
|
5730
|
+
};
|
|
5731
|
+
var keySchema = string().min(1).refine((value) => new TextEncoder().encode(value).length <= 256);
|
|
5703
5732
|
var stateQuerySchema = strictObject({
|
|
5704
5733
|
scope: _enum(["draft", "live"]),
|
|
5705
|
-
values:
|
|
5734
|
+
values: literal(false).optional()
|
|
5735
|
+
});
|
|
5736
|
+
var stateReadSchema = strictObject({
|
|
5737
|
+
scope: _enum(["draft", "live"]),
|
|
5738
|
+
keys: array(keySchema).min(1).max(stateReadLimits.maxKeys).refine((keys) => new Set(keys).size === keys.length, "State keys must be distinct.")
|
|
5706
5739
|
});
|
|
5707
5740
|
var stateClearSchema = strictObject({
|
|
5708
5741
|
scope: _enum(["draft", "live"]),
|
|
5709
|
-
key:
|
|
5742
|
+
key: keySchema.optional()
|
|
5710
5743
|
});
|
|
5711
5744
|
var dedupeClearSchema = strictObject({
|
|
5712
5745
|
scope: _enum(["draft", "live"]),
|
|
@@ -5776,11 +5809,10 @@ var flowStateViewSchema = object({
|
|
|
5776
5809
|
maxIdentities: number().positive(),
|
|
5777
5810
|
maxRunWrites: number().positive()
|
|
5778
5811
|
}),
|
|
5779
|
-
values: array(
|
|
5812
|
+
values: array(strictObject({
|
|
5780
5813
|
key: string(),
|
|
5781
5814
|
size: number().nonnegative(),
|
|
5782
|
-
updatedAt: number()
|
|
5783
|
-
value: json().optional()
|
|
5815
|
+
updatedAt: number()
|
|
5784
5816
|
})),
|
|
5785
5817
|
spaces: array(object({
|
|
5786
5818
|
space: string(),
|
|
@@ -5790,5 +5822,16 @@ var flowStateViewSchema = object({
|
|
|
5790
5822
|
updatedAt: number()
|
|
5791
5823
|
}))
|
|
5792
5824
|
});
|
|
5825
|
+
var flowStateValuesSchema = object({
|
|
5826
|
+
version: literal(1),
|
|
5827
|
+
scope: _enum(["draft", "live"]),
|
|
5828
|
+
values: array(strictObject({
|
|
5829
|
+
key: keySchema,
|
|
5830
|
+
value: json()
|
|
5831
|
+
})).min(1).max(stateReadLimits.maxKeys)
|
|
5832
|
+
}).refine((response) => {
|
|
5833
|
+
const encoder = new TextEncoder();
|
|
5834
|
+
return response.values.reduce((bytes, item) => bytes + (item.value === null ? 0 : encoder.encode(JSON.stringify(item.value)).byteLength), 0) <= stateReadLimits.maxBytes;
|
|
5835
|
+
}, "State values exceed the 1 MiB read limit.");
|
|
5793
5836
|
//#endregion
|
|
5794
|
-
export { StateError, dedupeClearSchema, dedupeIdentities, flowStateViewSchema, hashIdentities, skipDuplicatesConfigSchema, skipDuplicatesExecutorSchema, skipDuplicatesIdentities, skipDuplicatesTask, stateClearSchema, stateConformanceCases, stateKey, stateQuerySchema };
|
|
5837
|
+
export { StateError, dedupeClearSchema, dedupeIdentities, flowStateValuesSchema, flowStateViewSchema, hashIdentities, skipDuplicatesConfigSchema, skipDuplicatesExecutorSchema, skipDuplicatesIdentities, skipDuplicatesTask, stateClearSchema, stateConformanceCases, stateKey, stateQuerySchema, stateReadLimits, stateReadSchema };
|
package/dist/common/mcp.d.ts
CHANGED
|
@@ -22,7 +22,38 @@ export declare const mcpTools: {
|
|
|
22
22
|
validate(value: unknown): {
|
|
23
23
|
value: {
|
|
24
24
|
scope: "draft" | "live";
|
|
25
|
-
values?:
|
|
25
|
+
values?: false | undefined;
|
|
26
|
+
flowId: string;
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
issues: {
|
|
30
|
+
message: string;
|
|
31
|
+
path: string[];
|
|
32
|
+
}[];
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
flow_state_read: {
|
|
38
|
+
description: string;
|
|
39
|
+
annotations: {
|
|
40
|
+
readOnlyHint: boolean;
|
|
41
|
+
destructiveHint: boolean;
|
|
42
|
+
idempotentHint: boolean;
|
|
43
|
+
openWorldHint: boolean;
|
|
44
|
+
};
|
|
45
|
+
inputSchema: {
|
|
46
|
+
'~standard': {
|
|
47
|
+
version: 1;
|
|
48
|
+
vendor: string;
|
|
49
|
+
jsonSchema: {
|
|
50
|
+
input: () => Record<string, unknown>;
|
|
51
|
+
output: () => Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
validate(value: unknown): {
|
|
54
|
+
value: {
|
|
55
|
+
scope: "draft" | "live";
|
|
56
|
+
keys: string[];
|
|
26
57
|
flowId: string;
|
|
27
58
|
};
|
|
28
59
|
} | {
|
package/dist/common/mcp.js
CHANGED
|
@@ -8848,13 +8848,22 @@ var httpRequestConfigSchema = strictObject({
|
|
|
8848
8848
|
var httpRequestExecutorSchema = httpRequestConfigSchema.extend({ kind: literal("http-request") });
|
|
8849
8849
|
//#endregion
|
|
8850
8850
|
//#region src/state/common/state.ts
|
|
8851
|
+
var stateReadLimits = {
|
|
8852
|
+
maxKeys: 16,
|
|
8853
|
+
maxBytes: 1048576
|
|
8854
|
+
};
|
|
8855
|
+
var keySchema = string$1().min(1).refine((value) => new TextEncoder().encode(value).length <= 256);
|
|
8851
8856
|
var stateQuerySchema = strictObject({
|
|
8852
8857
|
scope: _enum(["draft", "live"]),
|
|
8853
|
-
values:
|
|
8858
|
+
values: literal(false).optional()
|
|
8859
|
+
});
|
|
8860
|
+
var stateReadSchema = strictObject({
|
|
8861
|
+
scope: _enum(["draft", "live"]),
|
|
8862
|
+
keys: array(keySchema).min(1).max(stateReadLimits.maxKeys).refine((keys) => new Set(keys).size === keys.length, "State keys must be distinct.")
|
|
8854
8863
|
});
|
|
8855
8864
|
var stateClearSchema = strictObject({
|
|
8856
8865
|
scope: _enum(["draft", "live"]),
|
|
8857
|
-
key:
|
|
8866
|
+
key: keySchema.optional()
|
|
8858
8867
|
});
|
|
8859
8868
|
var dedupeClearSchema = strictObject({
|
|
8860
8869
|
scope: _enum(["draft", "live"]),
|
|
@@ -8872,11 +8881,10 @@ var flowStateViewSchema = object({
|
|
|
8872
8881
|
maxIdentities: number().positive(),
|
|
8873
8882
|
maxRunWrites: number().positive()
|
|
8874
8883
|
}),
|
|
8875
|
-
values: array(
|
|
8884
|
+
values: array(strictObject({
|
|
8876
8885
|
key: string$1(),
|
|
8877
8886
|
size: number().nonnegative(),
|
|
8878
|
-
updatedAt: number()
|
|
8879
|
-
value: json$4().optional()
|
|
8887
|
+
updatedAt: number()
|
|
8880
8888
|
})),
|
|
8881
8889
|
spaces: array(object({
|
|
8882
8890
|
space: string$1(),
|
|
@@ -8886,6 +8894,17 @@ var flowStateViewSchema = object({
|
|
|
8886
8894
|
updatedAt: number()
|
|
8887
8895
|
}))
|
|
8888
8896
|
});
|
|
8897
|
+
var flowStateValuesSchema = object({
|
|
8898
|
+
version: literal(1),
|
|
8899
|
+
scope: _enum(["draft", "live"]),
|
|
8900
|
+
values: array(strictObject({
|
|
8901
|
+
key: keySchema,
|
|
8902
|
+
value: json$4()
|
|
8903
|
+
})).min(1).max(stateReadLimits.maxKeys)
|
|
8904
|
+
}).refine((response) => {
|
|
8905
|
+
const encoder = new TextEncoder();
|
|
8906
|
+
return response.values.reduce((bytes, item) => bytes + (item.value === null ? 0 : encoder.encode(JSON.stringify(item.value)).byteLength), 0) <= stateReadLimits.maxBytes;
|
|
8907
|
+
}, "State values exceed the 1 MiB read limit.");
|
|
8889
8908
|
function checkJsonDepth(value, depth = 0) {
|
|
8890
8909
|
if (depth > 64) throw new TypeError("Value exceeds the maximum JSON depth.");
|
|
8891
8910
|
if (value != null && typeof value == "object") for (const child of Object.values(value)) checkJsonDepth(child, depth + 1);
|
|
@@ -12362,10 +12381,22 @@ var ControlClient = class {
|
|
|
12362
12381
|
})
|
|
12363
12382
|
}));
|
|
12364
12383
|
}
|
|
12365
|
-
async getFlowState(flowId, scope
|
|
12366
|
-
const result = flowStateViewSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}
|
|
12384
|
+
async getFlowState(flowId, scope) {
|
|
12385
|
+
const result = flowStateViewSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}`));
|
|
12367
12386
|
return result.success && result.data.scope === scope ? result.data : invalidResponse();
|
|
12368
12387
|
}
|
|
12388
|
+
async getFlowStateValues(flowId, scope, keys, signal) {
|
|
12389
|
+
const result = flowStateValuesSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state/read`, {
|
|
12390
|
+
method: "POST",
|
|
12391
|
+
body: JSON.stringify({
|
|
12392
|
+
version: 1,
|
|
12393
|
+
scope,
|
|
12394
|
+
keys
|
|
12395
|
+
}),
|
|
12396
|
+
signal
|
|
12397
|
+
}));
|
|
12398
|
+
return result.success && result.data.scope === scope && result.data.values.length === keys.length && result.data.values.every((item, index) => item.key === keys[index]) ? result.data : invalidResponse();
|
|
12399
|
+
}
|
|
12369
12400
|
async clearFlowState(flowId, scope, key) {
|
|
12370
12401
|
const source = record(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}${key == null ? "" : `&key=${encodeURIComponent(key)}`}`, { method: "DELETE" }));
|
|
12371
12402
|
if (source.version !== 1 || source.cleared !== true) invalidResponse();
|
|
@@ -13128,7 +13159,8 @@ function tool(description, schema, readOnly, idempotent = true) {
|
|
|
13128
13159
|
};
|
|
13129
13160
|
}
|
|
13130
13161
|
var mcpTools = {
|
|
13131
|
-
flow_state: tool("Inspect Flow state usage, business
|
|
13162
|
+
flow_state: tool("Inspect Flow state usage, business key metadata and deduplication spaces. Never returns values. Scope is required.", stateQuerySchema.extend({ flowId: flow }), true),
|
|
13163
|
+
flow_state_read: tool("Read values for 1–16 distinct named State keys in one explicit scope. Values are returned in request order; missing keys return null. The combined stored JSON limit is 1 MiB; oversized reads fail without partial results.", stateReadSchema.extend({ flowId: flow }), true),
|
|
13132
13164
|
flow_state_clear: tool("Delete one business state key or clear all business values. Explicit draft/live scope is required; live changes production state immediately.", stateClearSchema.extend({ flowId: flow }), false),
|
|
13133
13165
|
flow_dedupe_clear: tool("Clear one deduplication space (flow or node:<id>). Explicit draft/live scope is required; live changes production history immediately.", dedupeClearSchema.extend({ flowId: flow }), false),
|
|
13134
13166
|
flow_canvas: tool("Broadcast a transient canvas command to connected Workbenches viewing this Flow. Success only confirms Server broadcast, not delivery or execution. No replay, session selection or Presentation save. Pan uses screen pixels (positive right/down); zoom is an absolute ratio (0.1–3); fit includes all nodes. Repeating pan moves again.", strictObject({
|
|
@@ -3,7 +3,10 @@ export { currentEngineContract, nodejsEngineContract, findEngineContract, type E
|
|
|
3
3
|
/** Serialized into the guest; keep validation and helpers self-contained. */
|
|
4
4
|
export declare function createState(invoke: (kind: string, payload: JsonValue) => Promise<RuntimeCapabilityResponse>): {
|
|
5
5
|
state: Readonly<{
|
|
6
|
-
get
|
|
6
|
+
get: {
|
|
7
|
+
(key: string): Promise<JsonValue>;
|
|
8
|
+
(keys: readonly string[]): Promise<JsonValue[]>;
|
|
9
|
+
};
|
|
7
10
|
set(key: unknown, value: unknown): Promise<void>;
|
|
8
11
|
insert(key: unknown, value: unknown): Promise<boolean>;
|
|
9
12
|
delete(key: unknown): Promise<void>;
|
|
@@ -120,11 +120,18 @@ function createState(invoke) {
|
|
|
120
120
|
if (!Array.isArray(ids) || !Array.from(ids).every((id) => typeof id === "string" || typeof id === "number" && Number.isFinite(id))) throw invalid("Identities must be strings or finite numbers.");
|
|
121
121
|
return ids;
|
|
122
122
|
};
|
|
123
|
+
async function get(key) {
|
|
124
|
+
if (Array.isArray(key)) {
|
|
125
|
+
if (key.length === 0 || key.length > 16) throw invalid("State reads require 1 to 16 keys.");
|
|
126
|
+
const keys = Array.from(key, keyOf);
|
|
127
|
+
if (new Set(keys).size !== keys.length) throw invalid("State keys must be distinct.");
|
|
128
|
+
return (await invoke("state.get", { key: keys })).body.values;
|
|
129
|
+
}
|
|
130
|
+
return (await invoke("state.get", { key: keyOf(key) })).body.value;
|
|
131
|
+
}
|
|
123
132
|
return {
|
|
124
133
|
state: Object.freeze({
|
|
125
|
-
|
|
126
|
-
return (await invoke("state.get", { key: keyOf(key) })).body.value;
|
|
127
|
-
},
|
|
134
|
+
get,
|
|
128
135
|
async set(key, value) {
|
|
129
136
|
await invoke("state.set", {
|
|
130
137
|
key: keyOf(key),
|
|
@@ -454,6 +461,46 @@ async function rejects(operation, pattern, message) {
|
|
|
454
461
|
throw new Error(`${message}: operation unexpectedly succeeded.`);
|
|
455
462
|
}
|
|
456
463
|
var runtimeConformanceCases = [
|
|
464
|
+
{
|
|
465
|
+
name: "reads multiple State keys in one Host call while preserving input order",
|
|
466
|
+
async verify(harness) {
|
|
467
|
+
const calls = [];
|
|
468
|
+
equal(await harness.invoke({
|
|
469
|
+
input: null,
|
|
470
|
+
invocationId: "state-batch-read",
|
|
471
|
+
capability: async ({ kind, payload }) => {
|
|
472
|
+
calls.push({
|
|
473
|
+
kind,
|
|
474
|
+
payload
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
status: 200,
|
|
478
|
+
body: { values: [
|
|
479
|
+
0,
|
|
480
|
+
null,
|
|
481
|
+
false,
|
|
482
|
+
""
|
|
483
|
+
] }
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
program: program(harness, `export default async (_, context) => context.state.get(['zero', 'missing', 'false', 'empty'])`)
|
|
487
|
+
}), [
|
|
488
|
+
0,
|
|
489
|
+
null,
|
|
490
|
+
false,
|
|
491
|
+
""
|
|
492
|
+
], "State batch values");
|
|
493
|
+
equal(calls, [{
|
|
494
|
+
kind: "state.get",
|
|
495
|
+
payload: { key: [
|
|
496
|
+
"zero",
|
|
497
|
+
"missing",
|
|
498
|
+
"false",
|
|
499
|
+
"empty"
|
|
500
|
+
] }
|
|
501
|
+
}], "State batch dispatch");
|
|
502
|
+
}
|
|
503
|
+
},
|
|
457
504
|
{
|
|
458
505
|
name: "cancels detached State work when its Task finishes",
|
|
459
506
|
async verify(harness) {
|
|
@@ -498,12 +545,20 @@ var runtimeConformanceCases = [
|
|
|
498
545
|
const invalid = [undefined, NaN, Infinity, new Date(), { x: undefined }, cyclic, [undefined], { x: () => 1 }];
|
|
499
546
|
const errors = [];
|
|
500
547
|
for (const value of invalid) { try { await context.state.set('key', value); errors.push('accepted'); } catch (e) { errors.push(e.code); } }
|
|
501
|
-
for (const call of [
|
|
548
|
+
for (const call of [
|
|
549
|
+
() => context.state.get(''), () => context.state.get('x'.repeat(257)),
|
|
550
|
+
() => context.state.get([]), () => context.state.get(['x', 'x']),
|
|
551
|
+
() => context.state.get(Array.from({length: 17}, (_, index) => String(index))),
|
|
552
|
+
() => context.state.get(['']), () => context.state.get(['汉'.repeat(86)]),
|
|
553
|
+
() => context.state.get(new Array(1)), () => context.state.get(null), () => context.state.get(),
|
|
554
|
+
() => context.state.insert('x', null), () => context.dedupe.insert([NaN]),
|
|
555
|
+
() => context.dedupe.has([null]), () => context.dedupe.insert(new Array(1))
|
|
556
|
+
]) {
|
|
502
557
|
try { await call(); errors.push('accepted'); } catch (e) { errors.push(e.code); }
|
|
503
558
|
}
|
|
504
559
|
return errors;
|
|
505
560
|
}`)
|
|
506
|
-
}), Array.from({ length:
|
|
561
|
+
}), Array.from({ length: 22 }, () => "capability.invalid"), "State boundary validation");
|
|
507
562
|
equal(calls, 0, "State invalid dispatch");
|
|
508
563
|
}
|
|
509
564
|
},
|
package/dist/common/scheduler.js
CHANGED
|
@@ -9535,13 +9535,24 @@ var httpRequestExecutorSchema = strictObject({
|
|
|
9535
9535
|
]).default("auto"),
|
|
9536
9536
|
statusPolicy: _enum(["success", "all"]).default("success")
|
|
9537
9537
|
}).extend({ kind: literal("http-request") });
|
|
9538
|
+
//#endregion
|
|
9539
|
+
//#region src/state/common/state.ts
|
|
9540
|
+
var stateReadLimits = {
|
|
9541
|
+
maxKeys: 16,
|
|
9542
|
+
maxBytes: 1048576
|
|
9543
|
+
};
|
|
9544
|
+
var keySchema = string$1().min(1).refine((value) => new TextEncoder().encode(value).length <= 256);
|
|
9545
|
+
strictObject({
|
|
9546
|
+
scope: _enum(["draft", "live"]),
|
|
9547
|
+
values: literal(false).optional()
|
|
9548
|
+
});
|
|
9538
9549
|
strictObject({
|
|
9539
9550
|
scope: _enum(["draft", "live"]),
|
|
9540
|
-
|
|
9551
|
+
keys: array(keySchema).min(1).max(stateReadLimits.maxKeys).refine((keys) => new Set(keys).size === keys.length, "State keys must be distinct.")
|
|
9541
9552
|
});
|
|
9542
9553
|
strictObject({
|
|
9543
9554
|
scope: _enum(["draft", "live"]),
|
|
9544
|
-
key:
|
|
9555
|
+
key: keySchema.optional()
|
|
9545
9556
|
});
|
|
9546
9557
|
strictObject({
|
|
9547
9558
|
scope: _enum(["draft", "live"]),
|
|
@@ -9558,11 +9569,10 @@ object({
|
|
|
9558
9569
|
maxIdentities: number().positive(),
|
|
9559
9570
|
maxRunWrites: number().positive()
|
|
9560
9571
|
}),
|
|
9561
|
-
values: array(
|
|
9572
|
+
values: array(strictObject({
|
|
9562
9573
|
key: string$1(),
|
|
9563
9574
|
size: number().nonnegative(),
|
|
9564
|
-
updatedAt: number()
|
|
9565
|
-
value: json$1().optional()
|
|
9575
|
+
updatedAt: number()
|
|
9566
9576
|
})),
|
|
9567
9577
|
spaces: array(object({
|
|
9568
9578
|
space: string$1(),
|
|
@@ -9572,6 +9582,17 @@ object({
|
|
|
9572
9582
|
updatedAt: number()
|
|
9573
9583
|
}))
|
|
9574
9584
|
});
|
|
9585
|
+
object({
|
|
9586
|
+
version: literal(1),
|
|
9587
|
+
scope: _enum(["draft", "live"]),
|
|
9588
|
+
values: array(strictObject({
|
|
9589
|
+
key: keySchema,
|
|
9590
|
+
value: json$1()
|
|
9591
|
+
})).min(1).max(stateReadLimits.maxKeys)
|
|
9592
|
+
}).refine((response) => {
|
|
9593
|
+
const encoder = new TextEncoder();
|
|
9594
|
+
return response.values.reduce((bytes, item) => bytes + (item.value === null ? 0 : encoder.encode(JSON.stringify(item.value)).byteLength), 0) <= stateReadLimits.maxBytes;
|
|
9595
|
+
}, "State values exceed the 1 MiB read limit.");
|
|
9575
9596
|
//#endregion
|
|
9576
9597
|
//#region src/flow/common/retry.ts
|
|
9577
9598
|
var retryNodeTypes = [
|
package/dist/index.d.ts
CHANGED
|
@@ -104,6 +104,7 @@ export interface TaskContext<Actions extends object = Record<string, never>, Wor
|
|
|
104
104
|
readonly blockId: string;
|
|
105
105
|
readonly state: {
|
|
106
106
|
get(key: string): Promise<JsonValue | null>;
|
|
107
|
+
get(keys: readonly string[]): Promise<(JsonValue | null)[]>;
|
|
107
108
|
set(key: string, value: JsonValue): Promise<void>;
|
|
108
109
|
insert(key: string, value: Exclude<JsonValue, null>): Promise<boolean>;
|
|
109
110
|
delete(key: string): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CanvasCommand } from '../../canvas/common/commands.ts'
|
|
2
|
-
import type { FlowStateView, StateScope } from '../../state/common/state.ts'
|
|
2
|
+
import type { FlowStateValues, FlowStateView, StateScope } from '../../state/common/state.ts'
|
|
3
3
|
|
|
4
4
|
import { currentEngineContract } from '../../runtime/common/engineContract.ts'
|
|
5
|
-
import { flowStateViewSchema } from '../../state/common/state.ts'
|
|
5
|
+
import { flowStateValuesSchema, flowStateViewSchema } from '../../state/common/state.ts'
|
|
6
6
|
import { workflowContractSchema, workflowReferencesSchema } from '../../workflow/common/workflow.ts'
|
|
7
7
|
export type { CanvasCommand, CanvasCommandSubscriber } from '../../canvas/common/commands.ts'
|
|
8
8
|
import type { LiveDetails } from './liveDetails.ts'
|
|
@@ -1017,11 +1017,23 @@ export class ControlClient {
|
|
|
1017
1017
|
return flow(await this.request(`/v1/flows/${segment(flowId)}`, { method: 'PATCH', body: JSON.stringify({ ...changes, version: 1 }) }))
|
|
1018
1018
|
}
|
|
1019
1019
|
|
|
1020
|
-
async getFlowState(flowId: string, scope: StateScope
|
|
1021
|
-
const result = flowStateViewSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}
|
|
1020
|
+
async getFlowState(flowId: string, scope: StateScope): Promise<FlowStateView> {
|
|
1021
|
+
const result = flowStateViewSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}`))
|
|
1022
1022
|
return result.success && result.data.scope === scope ? result.data : invalidResponse()
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
+
async getFlowStateValues(flowId: string, scope: StateScope, keys: readonly string[], signal?: AbortSignal): Promise<FlowStateValues> {
|
|
1026
|
+
const result = flowStateValuesSchema.safeParse(
|
|
1027
|
+
await this.request(`/v1/flows/${segment(flowId)}/state/read`, { method: 'POST', body: JSON.stringify({ version: 1, scope, keys }), signal }),
|
|
1028
|
+
)
|
|
1029
|
+
return result.success &&
|
|
1030
|
+
result.data.scope === scope &&
|
|
1031
|
+
result.data.values.length === keys.length &&
|
|
1032
|
+
result.data.values.every((item, index) => item.key === keys[index])
|
|
1033
|
+
? result.data
|
|
1034
|
+
: invalidResponse()
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1025
1037
|
async clearFlowState(flowId: string, scope: StateScope, key?: string): Promise<void> {
|
|
1026
1038
|
const source = record(
|
|
1027
1039
|
await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}${key == null ? '' : `&key=${encodeURIComponent(key)}`}`, { method: 'DELETE' }),
|
|
@@ -205,6 +205,37 @@ function liveRunRequest(harness: ControlApiConformanceHarness, publicationId: st
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
export const controlApiConformanceCases: readonly ControlApiConformanceCase[] = [
|
|
208
|
+
{
|
|
209
|
+
name: 'reads State metadata and bounded explicit keys without scope-wide value retrieval',
|
|
210
|
+
async verify(harness) {
|
|
211
|
+
const api = new ControlClient((url, init) => harness.request(new Request(new URL(url, harness.origin), init)))
|
|
212
|
+
const flow = await api.createFlow('State reads')
|
|
213
|
+
equal((await api.getFlowState(flow.flowId, 'draft')).values, [], 'New Flow State metadata')
|
|
214
|
+
equal(
|
|
215
|
+
await api.getFlowStateValues(flow.flowId, 'live', ['second', 'first']),
|
|
216
|
+
{
|
|
217
|
+
version: 1,
|
|
218
|
+
scope: 'live',
|
|
219
|
+
values: [
|
|
220
|
+
{ key: 'second', value: null },
|
|
221
|
+
{ key: 'first', value: null },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
'Missing named State keys',
|
|
225
|
+
)
|
|
226
|
+
const metadata = await request(harness, `/v1/flows/${flow.flowId}/state?scope=draft&values=false`)
|
|
227
|
+
await json(metadata, 200, 'Legacy metadata-only query')
|
|
228
|
+
equal(metadata.headers.get('cache-control'), 'no-store', 'State metadata cache policy')
|
|
229
|
+
await error(await request(harness, `/v1/flows/${flow.flowId}/state?scope=draft&values=true`), 400, 'flow.invalid', 'Scope-wide State values')
|
|
230
|
+
for (const input of [{ scope: 'draft', keys: [] }, { keys: ['key'] }, { scope: 'draft', keys: Array.from({ length: 17 }, (_, index) => String(index)) }])
|
|
231
|
+
await error(
|
|
232
|
+
await request(harness, `/v1/flows/${flow.flowId}/state/read`, { method: 'POST', body: JSON.stringify({ version: 1, ...input }) }),
|
|
233
|
+
400,
|
|
234
|
+
'flow.invalid',
|
|
235
|
+
'Invalid named State read',
|
|
236
|
+
)
|
|
237
|
+
},
|
|
238
|
+
},
|
|
208
239
|
{
|
|
209
240
|
name: 'imports workflow files atomically and exports a new unpublished draft with its Presentation',
|
|
210
241
|
async verify(harness) {
|
|
@@ -5,7 +5,7 @@ import { canvasCommandSchema } from '../../canvas/common/commands.ts'
|
|
|
5
5
|
import { runStatuses } from '../../execution/common/runLifecycle.ts'
|
|
6
6
|
import { waitCommentSchema } from '../../execution/common/wait.ts'
|
|
7
7
|
import { resourceNameIssue } from '../../flow/common/change.ts'
|
|
8
|
-
import { stateQuerySchema, stateClearSchema, dedupeClearSchema } from '../../state/common/state.ts'
|
|
8
|
+
import { stateQuerySchema, stateReadSchema, stateClearSchema, dedupeClearSchema } from '../../state/common/state.ts'
|
|
9
9
|
import { authoringReadSchema, authoringSearchSchema, authoringRequestSchema, authoringSchemaQuery } from './authoringSchema.ts'
|
|
10
10
|
import { resultQuerySchema } from './resultQuery.ts'
|
|
11
11
|
|
|
@@ -52,10 +52,15 @@ function tool<Args>(description: string, schema: z.ZodType<Args>, readOnly: bool
|
|
|
52
52
|
|
|
53
53
|
export const mcpTools = {
|
|
54
54
|
flow_state: tool(
|
|
55
|
-
'Inspect Flow state usage, business
|
|
55
|
+
'Inspect Flow state usage, business key metadata and deduplication spaces. Never returns values. Scope is required.',
|
|
56
56
|
stateQuerySchema.extend({ flowId: flow }),
|
|
57
57
|
true,
|
|
58
58
|
),
|
|
59
|
+
flow_state_read: tool(
|
|
60
|
+
'Read values for 1–16 distinct named State keys in one explicit scope. Values are returned in request order; missing keys return null. The combined stored JSON limit is 1 MiB; oversized reads fail without partial results.',
|
|
61
|
+
stateReadSchema.extend({ flowId: flow }),
|
|
62
|
+
true,
|
|
63
|
+
),
|
|
59
64
|
flow_state_clear: tool(
|
|
60
65
|
'Delete one business state key or clear all business values. Explicit draft/live scope is required; live changes production state immediately.',
|
|
61
66
|
stateClearSchema.extend({ flowId: flow }),
|
|
@@ -10,6 +10,7 @@ import { decodeDraftOperations, draftOperationsSchema } from './draftOperations.
|
|
|
10
10
|
export { decodeDraftOperations, draftOperationsSchema, resolveDraftOperations, type DraftOperation } from './draftOperations.ts'
|
|
11
11
|
import { z } from 'zod'
|
|
12
12
|
import { resourceNameIssue } from '../../flow/common/change.ts'
|
|
13
|
+
import { stateReadSchema } from '../../state/common/state.ts'
|
|
13
14
|
import { createEventSourceSchema, updateEventSourceSchema, eventSourceRevisionSchema } from './eventSourceSchemas.ts'
|
|
14
15
|
|
|
15
16
|
export const waitActionBodySchema: { parse(value: unknown): { comment?: string | null } } = z.strictObject({ comment: waitCommentSchema })
|
|
@@ -23,6 +24,7 @@ const tagName = id.transform((name) => name.trim()).refine((name) => resourceNam
|
|
|
23
24
|
const inputs = z.record(z.string(), z.record(z.string(), json))
|
|
24
25
|
const trigger = z.strictObject({ nodeId: id, outputs: z.record(z.string(), json) })
|
|
25
26
|
const schemas = {
|
|
27
|
+
readFlowState: stateReadSchema.extend({ version }),
|
|
26
28
|
canvas: z.strictObject({ command: canvasCommandSchema, version }),
|
|
27
29
|
authoringCheck: authoringCheckSchema,
|
|
28
30
|
authoringEdit: authoringRequestSchema,
|
|
@@ -78,6 +80,7 @@ function decoder<Value>(schema: z.ZodType<Value>): (value: unknown) => Value {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
export const controlRequests = {
|
|
83
|
+
readFlowState: decoder(schemas.readFlowState),
|
|
81
84
|
canvas: decoder(schemas.canvas),
|
|
82
85
|
authoringCheck: decoder(schemas.authoringCheck),
|
|
83
86
|
authoringEdit: decoder(schemas.authoringEdit),
|
|
@@ -27,11 +27,20 @@ export function createState(invoke: (kind: string, payload: JsonValue) => Promis
|
|
|
27
27
|
throw invalid('Identities must be strings or finite numbers.')
|
|
28
28
|
return ids
|
|
29
29
|
}
|
|
30
|
+
function get(key: string): Promise<JsonValue>
|
|
31
|
+
function get(keys: readonly string[]): Promise<JsonValue[]>
|
|
32
|
+
async function get(key: unknown) {
|
|
33
|
+
if (Array.isArray(key)) {
|
|
34
|
+
if (key.length === 0 || key.length > 16) throw invalid('State reads require 1 to 16 keys.')
|
|
35
|
+
const keys = Array.from(key, keyOf)
|
|
36
|
+
if (new Set(keys).size !== keys.length) throw invalid('State keys must be distinct.')
|
|
37
|
+
return ((await invoke('state.get', { key: keys })).body as { values: JsonValue[] }).values
|
|
38
|
+
}
|
|
39
|
+
return ((await invoke('state.get', { key: keyOf(key) })).body as { value: JsonValue }).value
|
|
40
|
+
}
|
|
30
41
|
return {
|
|
31
42
|
state: Object.freeze({
|
|
32
|
-
|
|
33
|
-
return ((await invoke('state.get', { key: keyOf(key) })).body as { value: JsonValue }).value
|
|
34
|
-
},
|
|
43
|
+
get,
|
|
35
44
|
async set(key: unknown, value: unknown) {
|
|
36
45
|
await invoke('state.set', { key: keyOf(key), value: valueOf(value) })
|
|
37
46
|
},
|
|
@@ -443,6 +452,23 @@ async function rejects(operation: Promise<unknown>, pattern: RegExp, message: st
|
|
|
443
452
|
}
|
|
444
453
|
|
|
445
454
|
export const runtimeConformanceCases: readonly RuntimeConformanceCase[] = [
|
|
455
|
+
{
|
|
456
|
+
name: 'reads multiple State keys in one Host call while preserving input order',
|
|
457
|
+
async verify(harness) {
|
|
458
|
+
const calls: { kind: string; payload: JsonValue }[] = []
|
|
459
|
+
const result = await harness.invoke({
|
|
460
|
+
input: null,
|
|
461
|
+
invocationId: 'state-batch-read',
|
|
462
|
+
capability: async ({ kind, payload }) => {
|
|
463
|
+
calls.push({ kind, payload })
|
|
464
|
+
return { status: 200, body: { values: [0, null, false, ''] } }
|
|
465
|
+
},
|
|
466
|
+
program: program(harness, `export default async (_, context) => context.state.get(['zero', 'missing', 'false', 'empty'])`),
|
|
467
|
+
})
|
|
468
|
+
equal(result, [0, null, false, ''], 'State batch values')
|
|
469
|
+
equal(calls, [{ kind: 'state.get', payload: { key: ['zero', 'missing', 'false', 'empty'] } }], 'State batch dispatch')
|
|
470
|
+
},
|
|
471
|
+
},
|
|
446
472
|
{
|
|
447
473
|
name: 'cancels detached State work when its Task finishes',
|
|
448
474
|
async verify(harness) {
|
|
@@ -492,7 +518,15 @@ export const runtimeConformanceCases: readonly RuntimeConformanceCase[] = [
|
|
|
492
518
|
const invalid = [undefined, NaN, Infinity, new Date(), { x: undefined }, cyclic, [undefined], { x: () => 1 }];
|
|
493
519
|
const errors = [];
|
|
494
520
|
for (const value of invalid) { try { await context.state.set('key', value); errors.push('accepted'); } catch (e) { errors.push(e.code); } }
|
|
495
|
-
for (const call of [
|
|
521
|
+
for (const call of [
|
|
522
|
+
() => context.state.get(''), () => context.state.get('x'.repeat(257)),
|
|
523
|
+
() => context.state.get([]), () => context.state.get(['x', 'x']),
|
|
524
|
+
() => context.state.get(Array.from({length: 17}, (_, index) => String(index))),
|
|
525
|
+
() => context.state.get(['']), () => context.state.get(['汉'.repeat(86)]),
|
|
526
|
+
() => context.state.get(new Array(1)), () => context.state.get(null), () => context.state.get(),
|
|
527
|
+
() => context.state.insert('x', null), () => context.dedupe.insert([NaN]),
|
|
528
|
+
() => context.dedupe.has([null]), () => context.dedupe.insert(new Array(1))
|
|
529
|
+
]) {
|
|
496
530
|
try { await call(); errors.push('accepted'); } catch (e) { errors.push(e.code); }
|
|
497
531
|
}
|
|
498
532
|
return errors;
|
|
@@ -501,7 +535,7 @@ export const runtimeConformanceCases: readonly RuntimeConformanceCase[] = [
|
|
|
501
535
|
})
|
|
502
536
|
equal(
|
|
503
537
|
result,
|
|
504
|
-
Array.from({ length:
|
|
538
|
+
Array.from({ length: 22 }, () => 'capability.invalid'),
|
|
505
539
|
'State boundary validation',
|
|
506
540
|
)
|
|
507
541
|
equal(calls, 0, 'State invalid dispatch')
|
|
@@ -12,6 +12,22 @@ export interface StateConformanceHarness {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const stateConformanceCases: readonly { name: string; verify(harness: StateConformanceHarness): Promise<void> }[] = [
|
|
15
|
+
{
|
|
16
|
+
name: 'reads selected State keys in input order with missing, falsy and independent values',
|
|
17
|
+
async verify(harness) {
|
|
18
|
+
const result = await harness.run(`export default async (_, context) => {
|
|
19
|
+
await context.state.set('object', { nested: [1] });
|
|
20
|
+
await context.state.set('zero', 0);
|
|
21
|
+
await context.state.set('false', false);
|
|
22
|
+
await context.state.set('empty', '');
|
|
23
|
+
const values = await context.state.get(['false', 'missing', 'object', 'zero', 'empty']);
|
|
24
|
+
values[2].nested.push(2);
|
|
25
|
+
return {result: {values, retained: await context.state.get('object'), zero: await context.state.get('zero')}};
|
|
26
|
+
}`)
|
|
27
|
+
if (!dequal(result, { values: [false, null, { nested: [1, 2] }, 0, ''], retained: { nested: [1] }, zero: 0 }))
|
|
28
|
+
throw new Error('State batch reads must preserve order, missing keys and independent JSON values.')
|
|
29
|
+
},
|
|
30
|
+
},
|
|
15
31
|
{
|
|
16
32
|
name: 'skips composite identities across successful Runs without depending on field order',
|
|
17
33
|
async verify(harness) {
|